<?php
/**
* @file
* @brief    sigplus Image Gallery Plus plug-in for Joomla
* @author   Levente Hunyadi
* @version  1.3.3
* @remarks  Copyright (C) 2009-2011 Levente Hunyadi
* @remarks  Licensed under GNU/GPLv3, see http://www.gnu.org/licenses/gpl-3.0.html
* @see      http://hunyadi.info.hu/projects/sigplus
*/

/*
* sigplus Image Gallery Plus plug-in for Joomla
* Copyright 2009-2010 Levente Hunyadi
*
* sigplus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sigplus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

// no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );

if (!defined('SIGPLUS_VERSION')) {
	define('SIGPLUS_VERSION', '1.3.3');
}

if (!defined('SIGPLUS_DEBUG')) {
	define('SIGPLUS_DEBUG', false);
}
if (!defined('SIGPLUS_LOGGING')) {
	define('SIGPLUS_LOGGING', false);
}

require_once dirname(__FILE__).DS.'exception.php';
require_once dirname(__FILE__).DS.'params.php';
require_once dirname(__FILE__).DS.'services.php';
require_once dirname(__FILE__).DS.'thumbs.php';
require_once dirname(__FILE__).DS.'engines.php';

function array_value($array, $key) {
	return isset($array[$key]) ? $array[$key] : false;
}

/**
* Builds HTML from tag name, attribute array and element content.
*/
function make_html($element, $attrs = false, $content = false) {
	$html = '<'.$element;
	if ($attrs !== false) {
		foreach ($attrs as $key => $value) {
			if ($value !== false) {
				$html .= ' '.$key.'="'.htmlspecialchars($value).'"';
			}
		}
	}
	if ($content !== false) {
		$html .= '>'.$content.'</'.$element.'>';
	} else {
		$html .= '/>';
	}
	return $html;
}

/**
* Returns the href attribute value of an anchor element.
*/
function get_anchor_attrs($html) {
	$matches = array();
	if (!preg_match('#<a\s([^<>]*)>#u', $html, $matches)) {
		return false;
	}
	$attrs = string_to_array($matches[1]);
	return $attrs;
}

/**
* Logging services.
*/
class SIGPlusLogging {
	/** Error log. */
	private $log = array();
	/** Singleton instance. */
	private static $inst = false;

	public static function instance() {
		if (self::$inst === false) {
			self::$inst = new SIGPlusLogging();
		}
		return self::$inst;
	}

	public function append($message) {
		$this->log[] = $message;
	}

	public function fetch() {
		ob_start();
			print '<ul>';
			foreach ($this->log as $logentry) {
				print '<li>'.$logentry.'</li>';
			}
			print '</ul>';
			$this->log = array();
		return ob_get_clean();
	}
}

class SIGPlusCoreConfiguration {
	/** Whether to utilize a content delivery network to load javascript frameworks. */
	public $ajaxapi = 'default';
	/** Whether to enter debug mode. */
	public $debug = false;

	public function validate() {
		if (is_string($this->ajaxapi)) {
			switch ($this->ajaxapi) {
				case 'none': case 'local': case 'cdn-google': case 'cdn-microsoft': case 'cdn-jquery':
					break;
				default:
					$this->ajaxapi = 'default';
			}
		} else {
			$this->ajaxapi = (bool) $this->ajaxapi ? 'default' : 'local';
		}
		$this->debug = (bool) $this->debug;
	}

	/**
	* Set parameter object from a Joomla JParameters object.
	*/
	public function setParameters(JRegistry $params) {
		$this->ajaxapi = $params->get('ajaxapi', 'default');
		$this->debug = $params->get('debug', false);
		$this->validate();
	}
}

/**
* System-wide global configuration settings.
*/
class SIGPlusConfiguration {
	public $core;
	public $services;
	public $galleries;

	public function setConfiguration(SIGPlusCoreConfiguration $core, SIGPlusImageServicesConfiguration $services, SIGPlusGalleryParameters $galleries) {
		$this->core = $core;
		$this->services = $services;
		$this->galleries = $galleries;
	}

	public function setParameters(JRegistry $params) {
		$this->core = new SIGPlusCoreConfiguration();  // global settings
		$this->core->setParameters($params);
		$this->services = new SIGPlusImageServicesConfiguration();  // image service settings
		$this->services->setParameters($params);
		$this->galleries = new SIGPlusGalleryParameters();  // administration back-end parameters
		$this->galleries->setParameters($params);
	}
}

/**
* sigplus Image Gallery Plus service class.
*/
class SIGPlusCore {
	/** General parameters. */
	private $imageservices;
	/** Associative array of default gallery-specific parameters. */
	private $defparams;
	/** Associative array of current gallery-specific parameters. */
	private $curparams;
	/** A list of identifiers issued. The list ensures uniqueness: duplicate identifiers are decorated to make them unique. */
	private static $galleryids = array();

    public function __construct(SIGPlusConfiguration $configuration = null) {
		// set general parameters
		$engineservices =& SIGPlusEngineServices::instance();
		$engineservices->ajaxapi = $configuration->core->ajaxapi;
		if (SIGPLUS_DEBUG) {  // force debug mode
			$engineservices->debug = true;
		} elseif (!isset($engineservices->debug)) {
			$engineservices->debug = (bool) $configuration->core->debug;  // do not disable debug mode if already set
		}

		// set default global parameters for image galleries
		$this->defparams = is_null($configuration) ? new SIGPlusGalleryParameters() : $configuration->galleries;
		if (SIGPLUS_LOGGING) {
			$logging =& SIGPlusLogging::instance();
			$logging->append('Global parameters are:<pre>'.print_r($this->defparams, true).'</pre>');
		}

		// create image services object
		try {
			$this->imageservices = new SIGPlusImageServices(is_null($configuration) ? null : $configuration->services);
		} catch (Exception $e) {
			$this->imageservices = null;  // image services not available
			throw $e;                     // re-throw exception
		}
	}

	/**
	* Creates a thumbnail image, a preview image, and a watermarked image for an original.
	* Images are generated only if they do not already exist.
	* A separate thumbnail image is generated if the preview is too large to act as a thumbnail.
	* @param string $imageref An absolute file system path or URL to an image.
	*/
	private function createPreviewImage($imageref) {
		$params = new SIGPlusPreviewParameters($this->curparams);
		$imagelibrary =& SIGPlusImageLibrary::instantiate($this->imageservices->getLibrary());

		// create watermarked image
		if (!is_remote_path($imageref) && $this->curparams->watermark && ($watermarkpath = $this->imageservices->getWatermarkPath(dirname($imageref))) !== false) {
			$watermarkedpath = $this->imageservices->getWatermarkedPath($imageref, $params);
			if ($watermarkedpath !== false && !is_file($watermarkedpath)) {
				$watermarkparams = $this->curparams->watermark_params;
				$watermarkparams['quality'] = $params->quality;  // GD cannot extract quality parameter from stored image, use quality set by user
				$result = $imagelibrary->createWatermarked($imageref, $watermarkpath, $watermarkedpath, $watermarkparams);
				if (SIGPLUS_LOGGING) {
					$logging =& SIGPlusLogging::instance();
					if ($result) {
						$logging->append('Saved watermarked image to <kbd>'.$watermarkedpath.'</kbd>');
					} else {
						$logging->append('Failed to save watermarked image to <kbd>'.$watermarkedpath.'</kbd>');
					}
				}
			}
		}

		// create preview image
		$previewpath = $this->imageservices->getPreviewPath($imageref, $params);
		if ($previewpath !== false && !is_file($previewpath)) {  // create image on-the-fly if not exists
			$result = $imagelibrary->createThumbnail($imageref, $previewpath, $params->width, $params->height, $params->crop, $params->quality);
			if (SIGPLUS_LOGGING) {
				$logging =& SIGPlusLogging::instance();
				if ($result) {
					$logging->append('Saved preview image to <kbd>'.$previewpath.'</kbd>');
				} else {
					$logging->append('Failed to save preview image to <kbd>'.$previewpath.'</kbd>');
				}
			}
		}

		// create thumbnail image
		$thumbpath = $this->imageservices->getThumbnailPath($imageref, $params);
		if ($thumbpath !== false && $thumbpath != $previewpath && !is_file($thumbpath)) {  // separate thumbnail image is required
			$thumbparams = $params->getThumbnailParameters();
			$result = $imagelibrary->createThumbnail($previewpath, $thumbpath, $thumbparams->width, $thumbparams->height, $thumbparams->crop, $thumbparams->quality);  // use preview image as source
			if (SIGPLUS_LOGGING) {
				$logging =& SIGPlusLogging::instance();
				if ($result) {
					$logging->append('Saved thumbnail to <kbd>'.$thumbpath.'</kbd>');
				} else {
					$logging->append('Failed to save thumbnail to <kbd>'.$thumbpath.'</kbd>');
				}
			}
		}
	}

	/**
	* Retrieves all data associated with an image.
	* @param string $imageref An absolute file system path or URL to an image.
	* @return An associative array of image (meta)data.
	*/
	private function getImageData($imageref, $index, $label = false) {
		// get lightbox
		$engineservices =& SIGPlusEngineServices::instance();
		$lightbox = $engineservices->getLightboxEngine($this->curparams->lightbox);  // get selected lightbox engine if any or use default

		// avoid generating HTML for those images that cannot be accessed
		if ($this->curparams->maxcount > 0 && $index >= $this->curparams->maxcount && !($lightbox && $lightbox->isQuickNavigationSupported())) {
			return false;
		}

		$isremote = is_remote_path($imageref);

		// get image thumbnail URL and parameters
		$params = new SIGPlusPreviewParameters($this->curparams);
		$thumburl = $this->imageservices->getThumbnailUrl($imageref, $params);
		$previewurl = $this->imageservices->getPreviewUrl($imageref, $params);

		// get image metadata
		if (!$isremote && $this->curparams->metadata) {
			require_once dirname(__FILE__).DS.'metadata.php';
			$metadata = SIGPlusIPTCServices::getImageMetadata($imageref);
		} else {
			$metadata = false;
		}

		// use caption and summary from labels file if available
		if ($label !== false) {
			$caption = $label->getCaptionHtml();
			$summary = $label->getDescriptionHtml();
		} else {
			$caption = false;
			$summary = false;
		}

		// use caption and summary from metadata if available
		if ($metadata !== false) {
			if (!$caption && isset($metadata['Headline'])) {
				if (is_array($metadata['Headline'])) {
					$caption = implode(';', $metadata['Headline']);
				} else {
					$caption = $metadata['Headline'];
				}
			}
			if (!$summary && isset($metadata['Caption-Abstract'])) {
				if (is_array($metadata['Caption-Abstract'])) {
					$summary = implode(';', $metadata['Caption-Abstract']);
				} else {
					$summary = $metadata['Caption-Abstract'];
				}
			}
		}

		// get target URL for preview image
		$url = false;
		if ($lightbox) {  // display lightbox pop-up window when thumbnail is clicked
			if ($isremote) {
				$url = $imageref;
			} else {
				if ($this->curparams->watermark) {
					$url = $this->imageservices->getWatermarkedUrl($imageref);
				} else {
					$url = $this->imageservices->getImageUrl($imageref, $this->curparams->authentication);
				}
			}
			$anchor_attrs = array('href' => $url);
		} elseif ($summary && ($anchor = get_anchor_attrs($summary)) !== false) {  // check if there is a hyperlink in the description and use it as target link
			$anchor_attrs = $anchor;
		}

		// get preview image parameters
		$img_attrs = array('preview' => $previewurl);
		if (!$this->curparams->crop) {
			$imagedims = is_remote_path($imageref) ? false : getimagesize($this->imageservices->getPreviewPath($imageref, $params));
			if ($imagedims !== false) {
				$params->width = $imagedims[0];
				$params->height = $imagedims[1];
			}
		}
		$img_attrs['width'] = $params->width;
		$img_attrs['height'] = $params->height;
		
		// get thumbnail image parameters
		if ($thumburl != $previewurl) {
			$img_attrs['thumb'] = $thumburl;
			$thumbparams = $params->getThumbnailParameters();
			$img_attrs['thumb-width'] = $thumbparams->width;
			$img_attrs['thumb-height'] = $thumbparams->height;
		}

		// set image caption and summary
		if ($caption) {
			$img_attrs['caption'] = $caption;
		}
		if ($summary) {
			$img_attrs['summary'] = $summary;
		}

		// get download URL
		if ($this->curparams->download && ($downloadurl = $this->imageservices->getFullsizeImageDownloadUrl($imageref, $this->curparams->authentication)) !== false) {
			$img_attrs['fullsize'] = $downloadurl;
		}

		if (SIGPLUS_LOGGING) {
			$logging =& SIGPlusLogging::instance();
			$logging->append('Thumbnail image URL is <kbd>'.$thumburl.'</kbd>');
			if ($metadata !== false) {
				$logging->append('Image metadata is available.');
			}
		}

		$imagedata = array(
			'image' => $img_attrs);
		if (isset($anchor_attrs)) {
			$imagedata['anchor'] = $anchor_attrs;
		}
		if ($metadata) {
			$imagedata['metadata'] = $metadata;
		}
		return $imagedata;
	}

	/**
	* Returns JavaScript code for a preview image in a gallery list.
	*/
	private function getPreviewScript($galleryid, $index, $imagedata) {
		$anchor_attrs = isset($imagedata['anchor']) ? $imagedata['anchor'] : false;
		$img_attrs = $imagedata['image'];

		return '['.implode(',',
			array(
				($anchor_attrs ? json_encode($anchor_attrs['href']) : 'null'),
				json_encode(isset($img_attrs['preview']) ? $img_attrs['preview'] : ''),
				$img_attrs['width'],
				$img_attrs['height'],
				json_encode(isset($img_attrs['thumb']) ? $img_attrs['thumb'] : ''),  // only with progressive loading enabled
				json_encode(isset($img_attrs['caption']) ? $img_attrs['caption'] : ''),
				json_encode(isset($img_attrs['summary']) ? $img_attrs['summary'] : ''),
				json_encode(isset($img_attrs['fullsize']) ? $img_attrs['fullsize'] : ''),
				json_encode(isset($imagedata['metadata']) ? $imagedata['metadata'] : null)
			)
		).']';
	}

	/**
	* Returns HTML code for a preview image in a gallery list.
	*/
	private function getPreviewHtml($galleryid, $index, $imagedata) {
		ob_start();

		if (isset($imagedata['anchor'])) {
			$anchor_attrs = $imagedata['anchor'];
		}
		$img_params = $imagedata['image'];

		$engineservices =& SIGPlusEngineServices::instance();
		$lightbox = $engineservices->getLightboxEngine($this->curparams->lightbox);  // get selected lightbox engine if any or use default

		// add rel attribute to hook lightbox
		if ($lightbox && isset($anchor_attrs)) {
			$anchor_attrs['rel'] = $lightbox->getLinkAttribute($galleryid);
		}

		// compose preview image (HTML img element)
		$imageid = $galleryid.'_img'.sprintf('%04d', $index);
		$img_attrs = array(
			'id' => $imageid,
			'width' => $img_params['width'],
			'height' => $img_params['height'],
			'src' => $img_params['preview']);
		if (isset($img_params['thumb'])) {  // a separate thumbnail image is available
			//$img_attrs['data-thumb'] = $img_params['thumb'];
			if ($this->curparams->maxcount > 0 && $index >= $this->curparams->maxcount) {
				// preview image would never be shown, image would only appear in lightbox navigation bar...
				$img_attrs['longdesc'] = $img_params['thumb'];
				// ...but lightbox can extract URL from longdesc attribute
				$img_attrs['src'] = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';  // fully transparent blank 1x1 image
			} elseif ($this->curparams->progressive) {  // a slider is present with progressive loading enabled
				$img_attrs['longdesc'] = $img_attrs['src'];
				$img_attrs['src'] = $img_params['thumb'];  // overriden with preview image by progressive loading
			}
		}
		if (!isset($img_params['caption']) && $this->curparams->deftitle !== false) {
			$img_params['caption'] = $this->curparams->deftitle;
		}
		$img_attrs['alt'] = isset($img_params['caption']) ? strip_tags($img_params['caption']) : '';
		if (!isset($img_params['summary']) && $this->curparams->defdescription !== false) {  // set default description if no description is supplied
			$img_params['summary'] = $this->curparams->defdescription;
		}
		if (isset($img_params['summary'])) {
			$summary = strip_tags($img_params['summary']);
			if (isset($anchor_attrs) && !isset($anchor_attrs['title'])) {
				$anchor_attrs['title'] = $summary;
			} else {
				$img_attrs['title'] = $summary;
			}
		}

		// compose metadata field (invisible HTML div element)
		ob_start();
			if (isset($img_params['caption']) && $img_params['caption'] != $img_attrs['alt']) {  // HTML has been stripped away
				print '<div id="'.$imageid.'_caption">'.$img_params['caption'].'</div>';
			}

			// summary text to display below image
			if (isset($img_params['summary'])) {
				print '<div id="'.$imageid.'_summary">'.$img_params['summary'].'</div>';
			}

			// image icons
			if (isset($img_params['fullsize'])) {
				print '<a rel="download" href="'.$img_params['fullsize'].'"></a>';
			}

			// image metadata
			if (isset($imagedata['metadata'])) {  // display IPTC image metadata in pop-up window if set
				print '<div id="'.$imageid.'_iptc">';
				print '<table>';
				foreach ($imagedata['metadata'] as $key => $value) {
					print '<tr><th>'.htmlspecialchars($key).'</th>';
					if (is_array($value)) {
						$stringvalue = implode(', ', $value);
					} else {
						$stringvalue = $value;
					}
					print '<td>'.nl2br(htmlspecialchars($stringvalue)).'</td></tr>';
				}
				print '</table>';
				print '</div>';
			}
		$meta = ob_get_clean();

		if ($this->curparams->maxcount > 0 && $index >= $this->curparams->maxcount) {  // images in excess of maximum thumbnail count
			print '<li style="display:none !important;">';  // images are shown in pop-up window but not on page
		} else {
			print '<li>';
		}
		$imagehtml = make_html('img', $img_attrs);
		if (isset($anchor_attrs)) {
			print make_html('a', $anchor_attrs, $imagehtml);
		} else {
			print $imagehtml;
		}
		if ($meta) {
			print '<div id="'.$imageid.'_metadata" style="display:none !important;">'.$meta.'</div>';
		}
		print '</li>';
		return ob_get_clean();
	}

	/**
	* Adds style and script declarations for an image gallery.
	*/
	private function addStylesAndScripts($galleryid) {
		// add styles and scripts for image gallery
		$engineservices =& SIGPlusEngineServices::instance();
		$engineservices->addStyleDefaultLanguage();
		$engineservices->addStyles();
		$lightbox = $engineservices->getLightboxEngine($this->curparams->lightbox);  // get selected lightbox engine if any, or use default
		if ($lightbox) {
			$lightbox->addStyles();
			$lightbox->addScripts($galleryid, $this->curparams);
		}
		if ($this->curparams->metadata) {
			$metadatabox = $engineservices->getMetadataEngine($this->curparams->lightbox);
			if ($metadatabox) {
				$metadatabox->addStyles();
				$metadatabox->addMetadataScripts();
			}
		}
		$slider = $engineservices->getSliderEngine($this->curparams->slider);  // get selected slider engine if any, or use default
		if ($slider) {  // use image thumbnail navigation controls
			$slider->addStyles();
			$slider->addScripts($galleryid, $this->curparams);
		}
		$captions = $engineservices->getCaptionsEngine($this->curparams->captions);  // get selected captions engine if any, or use default
		if ($captions) {
			if ($this->curparams->metadata) {
				$captions->showMetadata(true);
			}
			if ($this->curparams->download) {
				$captions->showDownload(true);
			}
			$captions->addStyles();
			$captions->addScripts($galleryid, $this->curparams);
		}

		// add custom style declaration based on back-end and inline settings
		$cssrules = array();
		if ($this->curparams->margin !== false) {
			$cssrules[] = 'margin:'.$this->curparams->margin.'px !important;';
		}
		if ($this->curparams->borderwidth !== false && $this->curparams->borderstyle !== false && $this->curparams->bordercolor !== false) {
			$cssrules[] = 'border:'.$this->curparams->borderwidth.'px '.$this->curparams->borderstyle.' #'.$this->curparams->bordercolor.' !important;';
		} else {
			if ($this->curparams->borderwidth !== false) {
				$cssrules[] = 'border-width:'.$this->curparams->borderwidth.'px !important;';
			}
			if ($this->curparams->borderstyle !== false) {
				$cssrules[] = 'border-style:'.$this->curparams->borderstyle.' !important;';
			}
			if ($this->curparams->bordercolor !== false) {
				$cssrules[] = 'border-color:#'.$this->curparams->bordercolor.' !important;';
			}
		}
		if ($this->curparams->padding !== false) {
			$cssrules[] = 'padding:'.$this->curparams->padding.'px !important;';
		}
		if (!empty($cssrules)) {
			$document =& JFactory::getDocument();
			$selectors = array(
				'ul > li img');
			if ($slider && ($selector = $slider->getImageStyleSelector())) {
				$selectors[] = $selector;
			}
			foreach ($selectors as &$selector) {
				$selector = '#'.$galleryid.' '.$selector;
			}
			$document->addStyleDeclaration(implode(', ', $selectors).' { '.implode("\n", $cssrules).' }');
		}
		//$document->addStyleDeclaration('#'.$galleryid.' ul > li { width: '.$this->curparams->width.'px; height: '.$this->curparams->height.'px; }');

		$engineservices->addOnReadyScripts();
	}

	/**
	* Generates an image gallery entirely defined with a list of label objects of remote URLs.
	* @param $list An array of label objects.
	*/
	private function getUserDefinedRemoteImageGalleryHtml(array $list, $galleryid, &$count) {
		$count = 0;
		foreach ($list as $listitem) {
			$this->createPreviewImage($listitem->imagefile);
			$count++;
		}

		$items = array();
		$index = 0;
		foreach ($list as $listitem) {
			if (($imagedata = $this->getImageData($listitem->imagefile, $index, $listitem)) === false) {
				continue;
			}
			switch ($this->curparams->linkage) {
				case 'inline':
					$items[] = $this->getPreviewHtml($galleryid, $index, $imagedata);
					break;
				default:
					$items[] = $this->getPreviewScript($galleryid, $index, $imagedata);
			}
			$index++;
		}
		return $items;
	}

	/**
	* Generates an image gallery entirely defined with a list of filenames or a list of label objects.
	* @param $imagedirectory An absolute path to a directory.
	* @param $list An array of filenames and/or label objects.
	*/
	private function getUserDefinedImageGalleryHtml($imagedirectory, array $list, $galleryid, &$count) {
		$count = 0;
		foreach ($list as $listitem) {
			if (is_string($listitem)) {
				$this->createPreviewImage($imagedirectory.DS.$listitem);
			} else {
				$this->createPreviewImage($imagedirectory.DS.$listitem->imagefile);
			}
			$count++;
		}

		$items = array();
		$index = 0;
		foreach ($list as $listitem) {
			if (is_string($listitem)) {
				$imagedata = $this->getImageData($imagedirectory.DS.$listitem, $index);
			} else {
				$imagedata = $this->getImageData($imagedirectory.DS.$listitem->imagefile, $index, $listitem);
			}
			if ($imagedata === false) {
				continue;
			}
			switch ($this->curparams->linkage) {
				case 'inline':
					$items[] = $this->getPreviewHtml($galleryid, $index, $imagedata);
					break;
				default:
					$items[] = $this->getPreviewScript($galleryid, $index, $imagedata);
			}
			$index++;
		}
		return $items;
	}

	/**
	* Generates an image gallery where some files have labels.
	* @param $imagedirectory An absolute path to a directory in the file system.
	*/
	private function getLabeledImageGalleryHtml($imagedirectory, $files, $labels, $galleryid, &$count) {
		if (empty($files)) {
			return false;
		}
		$labelmap = array();
		foreach ($labels as $label) {  // enumerate images listed in labels.txt
			$labelmap[$label->imagefile] = $label;
		}
		$files = array_filter($files, 'is_imagefile');
		$count = 0;
		foreach ($files as $file) {
			$this->createPreviewImage($imagedirectory.DS.$file);
			$count++;
		}

		$items = array();
		$index = 0;
		foreach ($files as $file) {
			if (($imagedata = $this->getImageData($imagedirectory.DS.$file, $index, array_value($labelmap, $file))) === false) {
				continue;
			}
			switch ($this->curparams->linkage) {
				case 'inline':
					$items[] = $this->getPreviewHtml($galleryid, $index, $imagedata);
					break;
				default:
					$items[] = $this->getPreviewScript($galleryid, $index, $imagedata);
			}
			$index++;
		}
		return $items;
	}

	/**
	* Generates an image gallery where files have no labels.
	* @param $imagedirectory An absolute path to a directory in the file system.
	*/
	private function getUnlabeledImageGalleryHtml($imagedirectory, $files, $galleryid, &$count) {
		return $this->getLabeledImageGalleryHtml($imagedirectory, $files, array(), $galleryid, $count);
	}

	/**
	* Ensures that a gallery identifier is unique across the page.
	* A gallery identifier is specified by the user or generated from the relative image path. Other extensions,
	* however, may duplicate article content on the page (e.g. show a short article extract in a module position),
	* making an identifier no longer unique. This function adds an ordinal to prevent conflicts when the same gallery
	* would occur multiple times on the page, causing scripts not to function properly.
	*/
	private function getUniqueGalleryId($galleryid) {
		if (in_array($galleryid, self::$galleryids)) {  // look for identifier in script-lifetime container
			$counter = 1000;
			do {
				$counter++;
				$gid = $galleryid.'_'.$counter;
			} while (in_array($gid, self::$galleryids));
			$galleryid = $gid;
		}
		self::$galleryids[] = $galleryid;
		return $galleryid;
	}

	/**
	* Generates image previews with alternate text, title and lightbox pop-up activation on mouse click.
	* @param string $body Data associated with the gallery.
	* @param string $paramstring A whitespace-separated list of name="value" parameter values.
	*/
	private function getImageGalleryHtml($body, $paramstring = '') {
		// set gallery parameters
		$this->curparams = clone $this->defparams;  // parameters set in back-end
		$paramstring = htmlspecialchars_decode($paramstring);
		$this->curparams->setString($paramstring);  // parameters set inline
		$engineservices =& SIGPlusEngineServices::instance();

		// generate link to an existing gallery
		if ($this->curparams->link !== false) {
			$lightbox = $engineservices->getLightboxEngine($this->curparams->lightbox);  // get selected lightbox engine if any or use default
			if ($lightbox && ($linkscript = $lightbox->getLinkScript($this->curparams->link)) !== false) {
				return '<a href="javascript:void('.htmlspecialchars($linkscript).')">'.$body.'</a>';
			} else {  // engine does not support programmatic activation
				return $body;
			}
		}

		// set gallery folders
		$imageref = $body;  // a relative path to an image folder or an image, or an absolute URL to an image to display
		if ($isremote = is_remote_path($imageref)) {
			$imageurl = $imageref;

			$imagehashbase = $imageurl;
		} else {
			$imageref = trim($imageref, '/');  // remove leading and trailing backslash

			// verify validity of relative path
			$imagepath = $this->imageservices->getImagePath($imageref);
			if (!file_exists($imagepath)) {
				throw new SIGPlusImageGalleryFolderException($imageref);
			}

			$imagehashbase = $imagepath;  // base in computing hash for content caching
		}

		// set gallery identifier
		if ($this->curparams->id) {  // use user-supplied identifier
			$galleryid = $this->curparams->id;
		} else {  // automatically generate identifier for thumbnail gallery
			$galleryid = 'sigplus_'.md5($imagehashbase);
		}
		$galleryid = $this->getUniqueGalleryId($galleryid);

		if ($this->curparams->layout != 'hidden' && ($isremote || is_file($imagepath))) {
			// force meaningful settings for single-image view (disable slider and activate flow layout)
			$this->curparams->layout = 'flow';
			$this->curparams->rows = false;
			$this->curparams->cols = false;
			$this->curparams->slider = false;
		}

		// initialize logging
		if (SIGPLUS_LOGGING) {
			$logging =& SIGPlusLogging::instance();
			if ($isremote) {
				$logging->append('Generating gallery "'.$galleryid.'" from URL: <kbd>'.$imageurl.'</kbd>');
			} else {
				$logging->append('Generating gallery "'.$galleryid.'" from file/directory: <kbd>'.$imagepath.'</kbd>');
			}
			$logging->append('Local parameters for "'.$galleryid.'" are:<pre>'.print_r($this->curparams, true).'</pre>');
		}

		// verify if content is available in cache
		if ($this->curparams->linkage == 'external' && ($cachekey = $this->imageservices->getCachedContent($imagehashbase, $this->curparams)) !== false) {
			if (SIGPLUS_LOGGING) {
				$logging->append('Retrieving content with key '.$cachekey.' from cache.');
			}
		} else {
			$cachekey = false;
		}

		// substitute proper left or right alignment depending on whether language is LTR or RTL
		$language =& JFactory::getLanguage();
		$this->curparams->alignment = str_replace(array('after','before'), $language->isRTL() ? array('left','right') : array('right','left'), $this->curparams->alignment);

		// set image gallery alignment (left, center or right)
		$gallerystyle = 'sigplus-gallery';
		switch ($this->curparams->alignment) {
			case 'left': case 'left-clear': case 'left-float': $gallerystyle .= ' sigplus-left'; break;
			case 'center': $gallerystyle .= ' sigplus-center'; break;
			case 'right': case 'right-clear': case 'right-float': $gallerystyle .= ' sigplus-right'; break;
		}
		switch ($this->curparams->alignment) {
			case 'left': case 'left-float': case 'right': case 'right-float': $gallerystyle .= ' sigplus-float'; break;
			case 'left-clear': case 'right-clear': $gallerystyle .= ' sigplus-clear'; break;
		}
		switch ($this->curparams->imagecaptions) {
			case 'above': $gallerystyle .= ' sigplus-captionsabove'; break;
			case 'below': $gallerystyle .= ' sigplus-captionsbelow'; break;
		}

		$slider = $engineservices->getSliderEngine($this->curparams->slider);  // get selected slider engine if any, or use default
		if (!$slider) {
			$this->curparams->progressive = false;  // progressive loading is not supported unless a slider is enabled
		}

		// generate gallery HTML code or setup script
		if ($cachekey === false) {
			$count = 0;
			if ($isremote) {  // access images remote domain
				$extension = strtolower(pathinfo(parse_url($imageurl, PHP_URL_PATH), PATHINFO_EXTENSION));
				switch ($extension) {
					case 'gif': case 'jpg': case 'jpeg': case 'png':  // plug-in syntax {gallery}http://example.com/image.jpg{/gallery}
						$labels = array(new SIGPlusImageLabel($imageurl, false, false));  // artificial single-entry labels file
						$htmlorscript = $this->getUserDefinedRemoteImageGalleryHtml($labels, $galleryid, $count);
						break;
					default:  // plug-in syntax {gallery}http://example.com{/gallery}
						throw new SIGPlusNotSupportedException();

						$labels = $this->imageservices->getLabels($imageurl, $this->curparams->labels, $this->curparams->deftitle, $this->curparams->defdescription);
						switch ($this->curparams->sortcriterion) {
							case SIGPLUS_SORT_RANDOMLABELS:
								shuffle($labels);
								// fall through
							case SIGPLUS_SORT_LABELS_OR_FILENAME:
							case SIGPLUS_SORT_LABELS_OR_MTIME:
								$htmlorscript = $this->getUserDefinedRemoteImageGalleryHtml($labels, $galleryid, $count);
						}
				}
			} else {
				if (is_file($imagepath)) {  // syntax {gallery}folder/subfolder/file.jpg{/gallery}
					$htmlorscript = $this->getUnlabeledImageGalleryHtml(dirname($imagepath), array(basename($imagepath)), $galleryid, $count);
				} else {  // syntax {gallery}folder/subfolder{/gallery}
					// fetch image labels
					switch ($this->curparams->labels) {
						case 'filename':
							$labels = $this->imageservices->getLabelsFromFilenames($imagepath); break;
						default:
							$labels = $this->imageservices->getLabels($imagepath, $this->curparams->labels, $this->curparams->deftitle, $this->curparams->defdescription);
					}
					switch ($this->curparams->sortcriterion) {
						case SIGPLUS_SORT_LABELS_OR_FILENAME:
							if (empty($labels)) {  // there is no labels file to use
								$files = $this->imageservices->getListing($imagepath, SIGPLUS_FILENAME, $this->curparams->sortorder, $this->curparams->depth);
								$htmlorscript = $this->getUnlabeledImageGalleryHtml($imagepath, $files, $galleryid, $count);
							} else {
								$htmlorscript = $this->getUserDefinedImageGalleryHtml($imagepath, $labels, $galleryid, $count);
							}
							break;
						case SIGPLUS_SORT_LABELS_OR_MTIME:
							if (empty($labels)) {
								$files = $this->imageservices->getListing($imagepath, SIGPLUS_MTIME, $this->curparams->sortorder, $this->curparams->depth);
								$htmlorscript = $this->getUnlabeledImageGalleryHtml($imagepath, $files, $galleryid, $count);
							} else {
								$htmlorscript = $this->getUserDefinedImageGalleryHtml($imagepath, $labels, $galleryid, $count);
							}
							break;
						case SIGPLUS_SORT_MTIME:
							$files = $this->imageservices->getListing($imagepath, SIGPLUS_MTIME, $this->curparams->sortorder, $this->curparams->depth);
							$htmlorscript = $this->getLabeledImageGalleryHtml($imagepath, $files, $labels, $galleryid, $count);
							break;
						case SIGPLUS_SORT_RANDOM:
							$files = @scandir($imagepath);
							if (!empty($files)) {
								shuffle($files);
							}
							$htmlorscript = $this->getLabeledImageGalleryHtml($imagepath, $files, $labels, $galleryid, $count);
							break;
						case SIGPLUS_SORT_RANDOMLABELS:
							if (empty($labels)) {  // there is no labels file to use
								$files = @scandir($imagepath);
								if (!empty($files)) {
									shuffle($files);
								}
								$htmlorscript = $this->getUnlabeledImageGalleryHtml($imagepath, $files, $galleryid, $count);
							} else {
								shuffle($labels);
								$htmlorscript = $this->getUserDefinedImageGalleryHtml($imagepath, $labels, $galleryid, $count);
							}
							break;
						default:  // case SIGPLUS_SORT_FILENAME:
							$files = $this->imageservices->getListing($imagepath, SIGPLUS_FILENAME, $this->curparams->sortorder, $this->curparams->depth);
							$htmlorscript = $this->getLabeledImageGalleryHtml($imagepath, $files, $labels, $galleryid, $count);
							break;
					}
				}
			}
		}

		if ($cachekey === false && empty($htmlorscript)) {
			$html = JText::_('SIGPLUS_EMPTY');
		} else {  // cached content also needs placeholder
			if ($this->curparams->linkage == 'inline') {  // content produced as HTML only in inline linkage mode
				$html = ($slider !== false ? '<ul style="visibility:hidden;">' : '<ul>').implode($htmlorscript).'</ul>';
			} else {
				$html = '';
			}
			$div_attrs = array(
				'id' => $galleryid,
				'class' => $gallerystyle);
			if ($this->curparams->layout == 'hidden') {
				$div_attrs['style'] = 'display:none !important;';
			}
			$html = make_html('div', $div_attrs, $html);
		}

		if ($cachekey !== false) {  // retrieve content from cache
			$this->addGalleryScript();
			$document =& JFactory::getDocument();
			$document->addScript($this->imageservices->getCachedContentUrl($cachekey));
		} else {
			if (!empty($htmlorscript) && $this->curparams->linkage != 'inline') {
				$galleryscript = $this->getGalleryScript($galleryid, $htmlorscript);
				switch ($this->curparams->linkage) {
					case 'external':  // save generated content for future re-use in a temporary file in the cache folder
						$this->addGalleryScript();
						$externalscript = '__jQuery__(function () { '.$galleryscript.' });';
						if ($cachekey = $this->imageservices->saveCachedContent($imagehashbase, $this->curparams, $externalscript)) {  // content successfully cached
							// include reference to generated script
							$document =& JFactory::getDocument();
							$document->addScript($this->imageservices->getCachedContentUrl($cachekey));
						} else {
							// put generated script in HTML head as a fall-back
							$engineservices->addOnReadyScript($galleryscript);
						}
						break;
					case 'head':  // put generated content in HTML head (does not allow HTML body with bloating size, which would cause preg_replace in System - SEF to fail)
						$this->addGalleryScript();
						$engineservices->addOnReadyScript($galleryscript);
						break;
				}
			}
		}

		// add style and script declarations
		$this->addStylesAndScripts($galleryid);

		$this->curparams = false;
		return $html;
	}

	/**
	* Adds JavaScript code that dynamically creating an image gallery from a data array.
	*/
	private function addGalleryScript() {
		$engineservices =& SIGPlusEngineServices::instance();
		$engineservices->addJQuery();
		$document =& JFactory::getDocument();
		$document->addScript(JURI::base(true).'/plugins/content/sigplus/js/linkage'.($engineservices->debug ? '.js' : '.min.js'));
	}

	private function getGalleryScript($galleryid, $script) {
		$engineservices =& SIGPlusEngineServices::instance();
		$lightbox = $engineservices->getLightboxEngine($this->curparams->lightbox);  // get selected lightbox engine if any or use default
		return '__jQuery__("#'.$galleryid.'").sigplusLinkage('.
			'['.implode(',', $script).'],'.
			'"'.($lightbox ? $lightbox->getLinkAttribute($galleryid) : '').'",'.  // rel attribute to hook lightbox
			$this->curparams->maxcount.','.
			($this->curparams->progressive ? 'true' : 'false').','.
			'"'.addslashes($this->curparams->deftitle).'",'.
			'"'.addslashes($this->curparams->defdescription).'");';
	}

	/**
	* Generates image thumbnails with alternate text, title and lightbox pop-up activation on mouse click.
	* Relative paths are interpreted w.r.t. the image base folder.
	* @param string $paramstring A whitespace-separated list of name="value" parameter values.
	*/
	public function getGalleryHtml($body, $paramstring = '') {
		if (!isset($this->imageservices)) {  // global error, image services are not available
			throw new SIGPlusInitializationException();
		}
		$oblevel = ob_get_level();
		try {
			return $this->getImageGalleryHtml($body, $paramstring);
		} catch (Exception $e) {  // local error
			for ($k = ob_get_level(); $k > $oblevel; $k--) {  // release output buffers
				ob_end_clean();
			}
			throw $e;  // re-throw exception
		}
	}

	/**
	* Adds activation code to a (fully customized) gallery.
	*/
	public function addGalleryEngines($customized = false) {
		if (!isset($this->imageservices)) {  // global error, image services not available
			throw new SIGPlusInitializationException();
		}
		$engineservices =& SIGPlusEngineServices::instance();
		if ($customized) {
			// hook anchors with image extensions to lightbox engine if any
			$engineservices->addStyles();
			$lightbox = $engineservices->getLightboxEngine($this->defparams->lightbox);  // get selected lightbox engine if any, or use default
			if ($lightbox) {
				$lightbox->addStyles();
				$lightbox->addActivationScripts();
			}
		}
		$engineservices->addOnReadyEvent();
	}
}