<?php
/**
 * @package     FOF
 * @copyright   2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license     GNU GPL version 2 or later
 */

namespace FOF30\Template;

use FOF30\Container\Container;
use FOF30\Less\Less;
use JDocument;

defined('_JEXEC') or die;

/**
 * A utility class to load view templates, media files and modules.
 *
 * @since    1.0
 */
class Template
{
	/**
	 * The component container
	 *
	 * @var  Container
	 */
	protected $container = null;

	/**
	 * Public constructor
	 *
	 * @param   Container  $container  The component container
	 */
	function __construct(Container $container)
	{
		$this->container = $container;
	}

	/**
	 * Add a CSS file to the page generated by the CMS
	 *
	 * @param   string  $uri      A path definition understood by parsePath, e.g. media://com_example/css/foo.css
	 * @param   string  $version  (optional) Version string to be added to the URL
	 * @param   string  $type     MIME type of the stylesheeet
	 * @param   string  $media    Media target definition of the style sheet, e.g. "screen"
	 * @param   array   $attribs  Array of attributes
	 *
	 * @see self::parsePath
	 *
	 * @return  void
	 */
	public function addCSS($uri, $version = null, $type = 'text/css', $media = null, $attribs = array())
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$method = 'addStyleSheet' . (!empty($version) ? 'Version' : '');

		$document = $this->container->platform->getDocument();

		if (!method_exists($document, $method))
		{
			return;
		}

		$url = $this->container->template->parsePath($uri);

		if (empty($version))
		{
			$document->addStyleSheet($url, $type, $media, $attribs);

			return;
		}

		$document->addStyleSheetVersion($url, $version, $type, $media, $attribs);
	}

	/**
	 * Add a JS script file to the page generated by the CMS.
	 *
	 * There are three combinations of defer and async (see http://www.w3schools.com/tags/att_script_defer.asp):
	 * * $defer false, $async true: The script is executed asynchronously with the rest of the page
	 *   (the script will be executed while the page continues the parsing)
	 * * $defer true, $async false: The script is executed when the page has finished parsing.
	 * * $defer false, $async false. (default) The script is loaded and executed immediately. When it finishes
	 *   loading the browser continues parsing the rest of the page.
	 *
	 * When you are using $defer = true there is no guarantee about the load order of the scripts. Whichever
	 * script loads first will be executed first. The order they appear on the page is completely irrelevant.
	 *
	 * @param   string   $uri      A path definition understood by parsePath, e.g. media://com_example/js/foo.js
	 * @param   boolean  $defer    Adds the defer attribute, see above
	 * @param   boolean  $async    Adds the async attribute, see above
	 * @param   string   $version  (optional) Version string to be added to the URL
	 * @param   string   $type     MIME type of the script
	 *
	 * @see self::parsePath
	 *
	 * @return  void
	 */
	public function addJS($uri, $defer = false, $async = false, $version = null, $type = 'text/javascript')
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$method = 'addScript' . (!empty($version) ? 'Version' : '');

		$document = $this->container->platform->getDocument();

		if (!method_exists($document, $method))
		{
			return;
		}

		$url = $this->container->template->parsePath($uri);

		if (empty($version))
		{
			$document->addScript($url, $type, $defer, $async);

			return;
		}

		$document->addScriptVersion($url, $version, $type, $defer, $async);
	}

	/**
	 * Compile a LESS file into CSS and add it to the page generated by the CMS.
	 * This method has integrated cache support. The compiled LESS files will be
	 * written to the media/lib_fof/compiled directory of your site. If the file
	 * cannot be written we will use the $altPath, if specified
	 *
	 * @param   string $path          A path definition understood by parsePath pointing to the source LESS file,
	 *                                e.g. media://com_example/less/foo.less
	 * @param   string   $altPath     A path definition understood by parsePath pointing to a precompiled CSS file,
	 *                                used when we can't write the generated file to the output directory,
	 *                                e.g. media://com_example/css/foo.css
	 * @param   boolean  $returnPath  Return the URL of the generated CSS file but do not include it. If it can't be
	 *                                generated, false is returned and the alt files are not included
	 * @param   string   $version     (optional) Version string to be added to the URL
	 * @param   string   $type        MIME type of the stylesheeet
	 * @param   string   $media       Media target definition of the style sheet, e.g. "screen"
	 * @param   array    $attribs     Array of attributes
	 *
	 * @see self::parsePath
	 *
	 * @since 2.0
	 *
	 * @return  mixed  True = successfully included generated CSS, False = the alternate CSS file was used, null = the source file does not exist
	 */
	public function addLESS($path, $altPath = null, $returnPath = false, $version = null, $type = 'text/css', $media = null, $attribs = array())
	{
		// Does the cache directory exists and is writeable
		static $sanityCheck = null;

		// Get the local LESS file
		$localFile = $this->parsePath($path, true);

		$filesystem   = $this->container->filesystem;
		$platformDirs = $this->container->platform->getPlatformBaseDirs();

		if (is_null($sanityCheck))
		{
			// Make sure the cache directory exists
			if (!is_dir($platformDirs['public'] . '/media/lib_fof/compiled/'))
			{
				$sanityCheck = $filesystem->folderCreate($platformDirs['public'] . '/media/lib_fof/compiled/');
			}
			else
			{
				$sanityCheck = true;
			}
		}

		// No point continuing if the source file is not there or we can't write to the cache

		if (!$sanityCheck || !is_file($localFile))
		{
			if (!$returnPath)
			{
				if (is_string($altPath))
				{
					$this->addCSS($altPath, $version, $type, $media, $attribs);
				}
				elseif (is_array($altPath))
				{
					foreach ($altPath as $anAltPath)
					{
						$this->addCSS($anAltPath, $version, $type, $media, $attribs);
					}
				}
			}

			return false;
		}

		// Get the source file's unique ID
		$id = md5(filemtime($localFile) . filectime($localFile) . $localFile);

		// Get the cached file path
		$cachedPath = $platformDirs['public'] . '/media/lib_fof/compiled/' . $id . '.css';

		// Get the LESS compiler
		$lessCompiler = new Less;
		$lessCompiler->formatterName = 'compressed';

		// Should I add an alternative import path?
		$altFiles = $this->getAltPaths($path);

		if (isset($altFiles['alternate']))
		{
			$currentLocation = realpath(dirname($localFile));
			$normalLocation = realpath(dirname($altFiles['normal']));
			$alternateLocation = realpath(dirname($altFiles['alternate']));

			if ($currentLocation == $normalLocation)
			{
				$lessCompiler->importDir = array($alternateLocation, $currentLocation);
			}
			else
			{
				$lessCompiler->importDir = array($currentLocation, $normalLocation);
			}
		}

		// Compile the LESS file
		$lessCompiler->checkedCompile($localFile, $cachedPath);

		// Add the compiled CSS to the page
		$base_url = rtrim($this->container->platform->URIbase(), '/');

		if (substr($base_url, -14) == '/administrator')
		{
			$base_url = substr($base_url, 0, -14);
		}

		$url = $base_url . '/media/lib_fof/compiled/' . $id . '.css';

		if ($returnPath)
		{
			return $url;
		}
		else
		{
			$this->addCSS($url, $version, $type, $media, $attribs);

			return true;
		}
	}

	/**
	 * Adds an inline JavaScript script to the page header
	 *
	 * @param   string  $script  The script content to add
	 * @param   string  $type    The MIME type of the script
	 */
	public function addJSInline($script, $type = 'text/javascript')
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$document = $this->container->platform->getDocument();

		if (!method_exists($document, 'addScriptDeclaration'))
		{
			return;
		}

		$document->addScriptDeclaration($script, $type);
	}

	/**
	 * Adds an inline stylesheet (inline CSS) to the page header
	 *
	 * @param   string  $css   The stylesheet content to add
	 * @param   string  $type  The MIME type of the script
	 */
	public function addCSSInline($css, $type = 'text/css')
	{
		if ($this->container->platform->isCli())
		{
			return;
		}

		$document = $this->container->platform->getDocument();

		if (!method_exists($document, 'addStyleDeclaration'))
		{
			return;
		}

		$document->addStyleDeclaration($css, $type);
	}

	/**
	 * Creates a SEF compatible sort header. Standard Joomla function will add a href="#" tag, so with SEF
	 * enabled, the browser will follow the fake link instead of processing the onSubmit event; so we
	 * need a fix.
	 *
	 * @param   string     $text   Header text
	 * @param   string     $field  Field used for sorting
	 * @param   \stdClass  $list   Object holding the direction and the ordering field
	 *
	 * @return  string  HTML code for sorting
	 */
	public function sefSort($text, $field, $list)
	{
		$sort = \JHTML::_('grid.sort', \JText::_(strtoupper($text)) . '&nbsp;', $field, $list->order_Dir, $list->order);

		return str_replace('href="#"', 'href="javascript:void(0);"', $sort);
	}

	/**
	 * Parse a fancy path definition into a path relative to the site's root,
	 * respecting template overrides, suitable for inclusion of media files.
	 * For example, media://com_foobar/css/test.css is parsed into
	 * media/com_foobar/css/test.css if no override is found, or
	 * templates/mytemplate/media/com_foobar/css/test.css if the current
	 * template is called mytemplate and there's a media override for it.
	 *
	 * Regarding plugins, templates are searched inside the plugin's tmpl directory and the template's html directory.
	 * For instance considering plugin://system/example/something the files will be looked for in:
	 * plugins/system/example/tmpl/something.php
	 * templates/yourTemplate/html/plg_system_example/something.php
	 * Template paths for plugins are uncommon and not standard Joomla! practice. They make sense when you are
	 * implementing features of your component as plugins and they need to provide HTML output, e.g. some of the
	 * integration plugins we use in Akeeba Subscriptions.
	 *
	 * The valid protocols are:
	 * media://		The media directory or a media override
	 * plugin://	Given as plugin://pluginType/pluginName/template, e.g. plugin://system/example/something
	 * admin://		Path relative to administrator directory (no overrides)
	 * site://		Path relative to site's root (no overrides)
	 *
	 * @param   string   $path       Fancy path
	 * @param   boolean  $localFile  When true, it returns the local path, not the URL
	 *
	 * @return  string  Parsed path
	 */
	public function parsePath($path, $localFile = false)
	{
		$platformDirs = $this->container->platform->getPlatformBaseDirs();

		if ($localFile)
		{
			$url = rtrim($platformDirs['root'], DIRECTORY_SEPARATOR) . '/';
		}
		else
		{
			$url = $this->container->platform->URIroot();
		}

		$altPaths = $this->getAltPaths($path);
		$filePath = $altPaths['normal'];

		// If JDEBUG is enabled, prefer that path, else prefer an alternate path if present
		if (defined('JDEBUG') && JDEBUG && isset($altPaths['debug']))
		{
			if (file_exists($platformDirs['public'] . '/' . $altPaths['debug']))
			{
				$filePath = $altPaths['debug'];
			}
		}
		elseif (isset($altPaths['alternate']))
		{
			if (file_exists($platformDirs['public'] . '/' . $altPaths['alternate']))
			{
				$filePath = $altPaths['alternate'];
			}
		}

		$url .= $filePath;

		return $url;
	}

	/**
	 * Parse a fancy path definition into a path relative to the site's root.
	 * It returns both the normal and alternative (template media override) path.
	 * For example, media://com_foobar/css/test.css is parsed into
	 * array(
	 *   'normal' => 'media/com_foobar/css/test.css',
	 *   'alternate' => 'templates/mytemplate/media/com_foobar/css//test.css'
	 * );
	 *
	 * The valid protocols are:
	 * media://		The media directory or a media override
	 * admin://		Path relative to administrator directory (no alternate)
	 * site://		Path relative to site's root (no alternate)
	 * auto://      Automatically guess if it should be site:// or admin://
	 * plugin://	The plugin directory or a template override (must be plugin://pluginType/pluginName/templateName)
	 *
	 * @param   string  $path  Fancy path
	 *
	 * @return  array  Array of normal and alternate parsed path
	 */
	public function getAltPaths($path)
	{
		$protoAndPath = explode('://', $path, 2);

		if (count($protoAndPath) < 2)
		{
			$protocol = 'media';
		}
		else
		{
			$protocol = $protoAndPath[0];
			$path = $protoAndPath[1];
		}

		if ($protocol == 'auto')
		{
			$protocol = $this->container->platform->isBackend() ? 'admin' : 'site';
		}

		$path = ltrim($path, '/' . DIRECTORY_SEPARATOR);

		switch ($protocol)
		{
			case 'media':
				// Do we have a media override in the template?
				$pathAndParams = explode('?', $path, 2);

				$ret = array(
					'normal'	 => 'media/' . $pathAndParams[0],
					'alternate'	 => $this->container->platform->getTemplateOverridePath('media:/' . $pathAndParams[0], false),
				);
				break;

			case 'plugin':
				// The path is pluginType/pluginName/viewTemplate
				$pathInfo = explode('/', $path);
				$pluginType = isset($pathInfo[0]) ? $pathInfo[0] : 'system';
				$pluginName = isset($pathInfo[1]) ? $pathInfo[1] : 'foobar';
				$viewTemplate = isset($pathInfo[2]) ? $pathInfo[2] : 'default';

				$pluginSystemName = 'plg_' . $pluginType . '_' . $pluginName;

				$ret = array(
					'normal'	 => 'plugins/' . $pluginType . '/' . $pluginName . '/tmpl/' . $viewTemplate,
					'alternate'	 => $this->container->platform->getTemplateOverridePath('auto:/' . $pluginSystemName . '/' . $viewTemplate, false),
				);

				break;

			case 'admin':
				$ret = array(
					'normal' => 'administrator/' . $path
				);
				break;

			default:
			case 'site':
				$ret = array(
					'normal' => $path
				);
				break;
		}

		// For CSS and JS files, add a debug path if the supplied file is compressed
		$filesystem = $this->container->filesystem;
		$ext        = $filesystem->getExt($ret['normal']);

		if (in_array($ext, array('css', 'js')))
		{
			$file = basename($filesystem->stripExt($ret['normal']));

			/*
			 * Detect if we received a file in the format name.min.ext
			 * If so, strip the .min part out, otherwise append -uncompressed
			 */

			if (strlen($file) > 4 && strrpos($file, '.min', '-4'))
			{
				$position = strrpos($file, '.min', '-4');
				$filename = str_replace('.min', '.', $file, $position) . $ext;
			}
			else
			{
				$filename = $file . '-uncompressed.' . $ext;
			}

			// Clone the $ret array so we can manipulate the 'normal' path a bit
			$t1 = (object) $ret;
			$temp = clone $t1;
			unset($t1);
			$temp = (array)$temp;
			$normalPath = explode('/', $temp['normal']);
			array_pop($normalPath);
			$normalPath[] = $filename;
			$ret['debug'] = implode('/', $normalPath);
		}

		return $ret;
	}

	/**
	 * Returns the contents of a module position
	 *
	 * @param   string  $position  The position name, e.g. "position-1"
	 * @param   int     $style     Rendering style, see JDocumentRendererModule::render
	 *
	 * @return  string  The contents of the module position
	 */
	public function loadPosition($position, $style = -2)
	{
		$document = $this->container->platform->getDocument();

		if (!($document instanceof JDocument))
		{
			return '';
		}

		if (!method_exists($document, 'loadRenderer'))
		{
			return '';
		}

		try
		{
			$renderer = $document->loadRenderer('module');
		}
		catch (\Exception $exc)
		{
			return '';
		}

		$params = array('style' => $style);

		$contents = '';

		foreach (\JModuleHelper::getModules($position) as $mod)
		{
			$contents .= $renderer->render($mod, $params);
		}

		return $contents;
	}

	/**
	 * Render a module by name
	 *
	 * @param   string  $moduleName  The name of the module (real, eg 'Breadcrumbs' or folder, eg 'mod_breadcrumbs')
	 * @param   int     $style       The rendering style, see JDocumentRendererModule::render
	 *
	 * @return string  The rendered module
	 */
	public function loadModule($moduleName, $style = -2)
	{
		$document = $this->container->platform->getDocument();

		if (!($document instanceof JDocument))
		{
			return '';
		}

		if (!method_exists($document, 'loadRenderer'))
		{
			return '';
		}

		try
		{
			$renderer = $document->loadRenderer('module');
		}
		catch (\Exception $exc)
		{
			return '';
		}

		$params = array('style' => $style);

		$mod = \JModuleHelper::getModule($moduleName);

		if (empty($mod))
		{
			return '';
		}

		return $renderer->render($mod, $params);
	}

	/**
	 * Merges the current url with new or changed parameters.
	 *
	 * This method merges the route string with the url parameters defined
	 * in current url. The parameters defined in current url, but not given
	 * in route string, will automatically reused in the resulting url.
	 * But only these following parameters will be reused:
	 *
	 * option, view, layout, format
	 *
	 * Example:
	 *
	 * Assuming that current url is:
	 * http://fobar.com/index.php?option=com_foo&view=cpanel
	 *
	 * <code>
	 * <?php echo F0FTemplateutils::route('view=categories&layout=tree'); ?>
	 * </code>
	 *
	 * Result:
	 * http://fobar.com/index.php?option=com_foo&view=categories&layout=tree
	 *
	 * @param   string  $route  The parameters string
	 *
	 * @return  string  The human readable, complete url
	 */
	public function route($route = '')
	{
		$route = trim($route);

		// Special cases

		if ($route == 'index.php' || $route == 'index.php?')
		{
			$result = $route;
		}
		elseif (substr($route, 0, 1) == '&')
		{
			$url = \JURI::getInstance();
			$vars = array();
			parse_str($route, $vars);

			$url->setQuery(array_merge($url->getQuery(true), $vars));

			$result = 'index.php?' . $url->getQuery();
		}
		else
		{
			$url = \JURI::getInstance();
			$props = $url->getQuery(true);

			// Strip 'index.php?'
			if (substr($route, 0, 10) == 'index.php?')
			{
				$route = substr($route, 10);
			}

			// Parse route
			$parts = array();
			parse_str($route, $parts);
			$result = array();

			// Check to see if there is component information in the route if not add it

			if (!isset($parts['option']) && isset($props['option']))
			{
				$result[] = 'option=' . $props['option'];
			}

			// Add the layout information to the route only if it's not 'default'

			if (!isset($parts['view']) && isset($props['view']))
			{
				$result[] = 'view=' . $props['view'];

				if (!isset($parts['layout']) && isset($props['layout']))
				{
					$result[] = 'layout=' . $props['layout'];
				}
			}

			// Add the format information to the URL only if it's not 'html'

			if (!isset($parts['format']) && isset($props['format']) && $props['format'] != 'html')
			{
				$result[] = 'format=' . $props['format'];
			}

			// Reconstruct the route

			if (!empty($route))
			{
				$result[] = $route;
			}

			$result = 'index.php?' . implode('&', $result);
		}

		return \JRoute::_($result);
	}
}