<?php
	/**
	 * An abstract utility class to handle Html tag rendering, as well as utilities to render
	 * pieces of HTML and CSS code.  All methods are static.
	 */
	abstract class QHtml {

		const IsVoid = true;

		// Common URL Protocols
		/** HTTP Protocol */
		const HTTP = 'http://';
		/** HTTPS Protocol */
		const HTTPS = 'https://';
		/** FTP Protocol */
		const FTP = 'ftp://';
		/** SFTP Protocol */
		const SFTP = 'sftp://';
		/** SMB Protocol */
		const SMB = 'smb://';


		/**
		 * This faux constructor method throws a caller exception.
		 * The Css object should never be instantiated, and this constructor
		 * override simply guarantees it.
		 *
		 * @throws QCallerException
		 * @return QHtml
		 */
		public final function __construct() {
			throw new QCallerException('QHtml should never be instantiated.  All methods and variables are publicly statically accessible.');
		}

		/**
		 * Renders an html tag with the given attributes and inner html.
		 *
		 * If the innerHtml is detected as being wrapped in an html tag of some sort, it will attempt to format the code so that
		 * it has a structured view in a browser, with the inner html indented and on a new line in between the tags. You
		 * can turn this off by setting __MINIMIZE__, or by passing in true to $blnNoSpace.
		 *
		 * There area a few special cases to consider:
		 * - Void elements will not be formatted to avoid adding unnecessary white space since these are generally
		 *   inline elements
		 * - Non-void elements always use internal newlines, even in __MINIMIZE__ mode. This is to prevent different behavior
		 *   from appearing in __MINIMIZE__ mode on inline elements, because inline elements with internal space will render with space to separate
		 *   from surrounding elements. Usually, this is not an issue, but in the special situations where you really need inline
		 *   elements to be right up against its siblings, set $blnNoSpace to true.
		 *
		 *
		 * @param string 		$strTag				The tag name
		 * @param null|mixed 	$mixAttributes 		String of attribute values or array of attribute values.
		 * @param null|string 	$strInnerHtml 		The html to print between the opening and closing tags. This will NOT be escaped.
		 * @param boolean		$blnIsVoidElement 	True to print as a tag with no closing tag.
		 * @param boolean		$blnNoSpace		 	Renders with no white-space. Useful in special inline situations.
		 * @return string						The rendered html tag
		 */
		public static function RenderTag($strTag, $mixAttributes, $strInnerHtml = null, $blnIsVoidElement = false, $blnNoSpace = false) {
			assert ('!empty($strTag)');
			$strToReturn = '<' . $strTag;
			if ($mixAttributes) {
				if (is_string($mixAttributes)) {
					$strToReturn .=  ' ' . trim($mixAttributes);
				} else {
					// assume array
					$strToReturn .=  QHtml::RenderHtmlAttributes($mixAttributes);
				}
			};
			if ($blnIsVoidElement) {
				$strToReturn .= ' />'; // conforms to both XHTML and HTML5 for both normal and foreign elements
			}
			elseif ($blnNoSpace || substr (trim($strInnerHtml), 0, 1) !== '<') {
				$strToReturn .= '>' . $strInnerHtml . '</' . $strTag . '>';
			}
			else {
				// the hardcoded newlines below are important to prevent different drawing behavior in MINIMIZE mode
				$strToReturn .= '>' . "\n" . _indent(trim($strInnerHtml)) .  "\n" . '</' . $strTag . '>' . _nl();
			}
			return $strToReturn;
		}

		/**
		 * Renders an input element with a label tag. Uses separate styling for the label and the input object.
		 * In particular, this gives you the option of wrapping the input with a label (which is what Bootstrap
		 * expects on checkboxes) or putting the label next to the object (which is what jQueryUI expects).
		 *
		 * Note that if you are not setting $blnWrapped, it is up to you to insert the "for" attribute into
		 * the label attributes.
		 *
		 * @param $strLabel
		 * @param $blnTextLeft
		 * @param $strAttributes
		 * @param $strLabelAttributes
		 * @param $blnWrapped
		 * @return string
		 */
		public static function RenderLabeledInput($strLabel, $blnTextLeft, $strAttributes, $strLabelAttributes, $blnWrapped) {
			$strHtml = trim(self::RenderTag('input', $strAttributes, null, true));

			if ($blnWrapped) {
				if ($blnTextLeft) {
					$strCombined = $strLabel .  $strHtml;
				} else {
					$strCombined = $strHtml . $strLabel;
				}

				$strHtml = self::RenderTag('label', $strLabelAttributes, $strCombined);
			}
			else {
				$strLabel = trim(self::RenderTag('label', $strLabelAttributes, $strLabel));
				if ($blnTextLeft) {
					$strHtml = $strLabel .  $strHtml;
				} else {
					$strHtml = $strHtml . $strLabel;
				}
			}
			return $strHtml;
		}

		/**
		 * Returns the formatted value of type <length>.
		 * See http://www.w3.org/TR/CSS1/#units for more info.
		 * @param 	string 	$strValue 	The number or string to be formatted to the <length> compatible value.
		 * @return 	string 	the formatted value of type <length>.
		 */
		public final static function FormatLength($strValue) {
			if (is_numeric($strValue)) {
				if (0 == $strValue) {
					if (!is_int($strValue)) {
						$fltValue = floatval($strValue);
						return sprintf('%s', $fltValue);
					} else {
						return sprintf('%s', $strValue);
					}
				} else {
					if (!is_int($strValue)) {
						$fltValue = floatval($strValue);
						return sprintf('%spx', $fltValue);
					} else {
						return sprintf('%spx', $strValue);
					}
				}
			} else {
				return sprintf('%s', $strValue);
			}
		}

		/**
		 * Sets the given length string to the new length value.
		 * If the new length is preceded by a math operator (+-/*), then arithmetic is performed on the previous
		 * value. Returns true if the length changed.
		 * @param 	string 	$strOldLength
		 * @param 	string 	$newLength
		 * @return 	bool	true if the length was changed
		 */
		public static function SetLength(&$strOldLength, $newLength) {
			if ($newLength && preg_match('#^(\+|\-|/|\*)(.+)$#',$newLength, $matches)) { // do math operation
				$strOperator = $matches[1];
				$newValue = $matches[2];
				assert (is_numeric($newValue));
				if (!$strOldLength) {
					$oldValue  = 0;
					$oldUnits = 'px';
				} else {
					$oldValue = filter_var ($strOldLength, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
					if (preg_match('/([A-Z]+|[a-z]+|%)$/', $strOldLength, $matches)) {
						$oldUnits = $matches[1];
					} else {
						$oldUnits = 'px';
					}
				}

				switch ($strOperator) {
					case '+':
						$newValue = $oldValue + $newValue;
						break;

					case '-':
						$newValue = $oldValue - $newValue;
						break;

					case '/':
						$newValue = $oldValue / $newValue;
						break;

					case '*':
						$newValue = $oldValue * $newValue;
						break;
				}
				if ($newValue != $oldValue) {
					$strOldLength = $newValue . $oldUnits; // update returned value
					return true;
				} else {
					return false; // nothing changed
				}
			} else { // no math operation
				$newLength = self::FormatLength($newLength);

				if ($strOldLength !== $newLength) {
					$strOldLength = $newLength;
					return true;
				} else {
					return false;
				}
			}
		}


		/**
		 * Helper to add a class or classes to a pre-existing space-separated list of classes. Checks to make sure the
		 * class isn't already in the list. Returns true to indicate a change in the list.
		 *
		 * @param string 	$strClassList	Current list of classes separated by a space
		 * @param string 	$strNewClasses 	New class to add. Could be a list separated by spaces.
		 * @return bool 	true if the class list was changed.
		 */
		public static function AddClass(&$strClassList, $strNewClasses) {
			$strNewClasses = trim($strNewClasses);
			if (empty($strNewClasses)) return false;

			if (empty ($strClassList)) {
				$strCurrentClasses = array();
			}
			else {
				$strCurrentClasses = explode(' ', $strClassList);
			}

			$blnChanged = false;
			foreach (explode (' ', $strNewClasses) as $strClass) {
				if ($strClass && !in_array ($strClass, $strCurrentClasses)) {
					$blnChanged = true;
					if (!empty ($strClassList)) {
						$strClassList .= ' ';
					}
					$strClassList .= $strClass;
				}
			}

			return $blnChanged;
		}

		/**
		 * Helper to remove a class or classes from a list of space-separated classes.
		 *
		 * @param string $strClassList        class list string to search
		 * @param string $strCssNamesToRemove space separated list of names to remove
		 *
		 * @return bool    true if the class list was changed
		 */
		public static function RemoveClass(&$strClassList, $strCssNamesToRemove) {
			$strNewCssClass = '';
			$blnRemoved = false;
			$strCssNamesToRemove = trim($strCssNamesToRemove);
			if (empty($strCssNamesToRemove)) return false;

			if (empty ($strClassList)) {
				$strCurrentClasses = array();
			}
			else {
				$strCurrentClasses = explode(' ', $strClassList);
			}
			$strRemoveArray = explode (' ', $strCssNamesToRemove);

			foreach ($strCurrentClasses as $strCssClass) {
				if ($strCssClass = trim($strCssClass)) {
					if (in_array($strCssClass, $strRemoveArray)) {
						$blnRemoved = true;
					}
					else {
						$strNewCssClass .= $strCssClass . ' ';
					}
				}
			}
			if ($blnRemoved) {
				$strClassList = trim($strNewCssClass);
			}
			return $blnRemoved;
		}

		/**
		 * Many CSS frameworks use families of classes, which are built up from a base family name. For example,
		 * Bootstrap uses 'col-lg-6' to represent a column that is 6 units wide on large screens and Foundation
		 * uses 'large-6' to do the same thing. This utility removes classes that start with a particular prefix
		 * to remove whatever sizing class was specified.
		 *
		 * @param  $strClassList
		 * @param  $strPrefix
		 * @return bool true if the class list changed
		 */
		public static function RemoveClassesByPrefix (&$strClassList, $strPrefix) {
			$aRet = array();
			$blnChanged = false;
			if ($strClassList) foreach (explode (' ', $strClassList) as $strClass) {
				if (strpos($strClass, $strPrefix) !== 0) {
					$aRet[] = $strClass;
				}
				else {
					$blnChanged = true;
				}
			}
			$strClassList = implode (' ', $aRet);
			return $blnChanged;
		}

		/**
		 * Render the given attribute array for html output. Escapes html entities enclosed in values. Uses
		 * double-quotes to surround the value. Precedes the resulting text with a space character.
		 *
		 * @param array|null $attributes
		 * @return string
		 */
		public static function RenderHtmlAttributes ($attributes) {
			$strToReturn = '';
			if ($attributes) {
				foreach ($attributes as $strName=>$strValue) {
					if ($strValue === false) {
						$strToReturn .= (' ' . $strName);
					} elseif (!is_null($strValue)) {
						$strToReturn .= (' ' . $strName . '="' . htmlspecialchars($strValue, ENT_COMPAT | ENT_HTML5, QApplication::$EncodingType) . '"');
					}
				}
			}
			return $strToReturn;
		}


		/**
		 * Render the given array as a css style string. It will NOT be escaped.
		 *
		 * @param array 	$styles		key/value array representing the styles.
		 * @return string	a string suitable for including in a css 'style' property
		 */
		public static function RenderStyles($styles) {
			if (!$styles) return '';
			return implode('; ', array_map(
				function ($v, $k) { return $k . ':' . $v; },
				$styles,
				array_keys($styles))
			);
		}

		/**
		 * Returns the given string formatted as an html comment that will go on its own line.
		 * @param string 	$strText
		 * @param bool 		$blnRemoveOnMinimize
		 * @return string
		 */
		public static function Comment($strText, $blnRemoveOnMinimize = true) {
			if ($blnRemoveOnMinimize && QApplication::$Minimize) {
				return '';
			}
			return  _nl() . '<!-- ' . $strText . ' -->' . _nl();

		}

		/**
		 * Generate a URL from components. This URL can be used in the QApplication::Redirect function, or applied to
		 * an anchor tag by setting the href attribute.
		 *
		 * You can also use this to modify a URL by passing a complete URL in the location. The URL will be modified by the parameters given.
		 *
		 * @param string $strLocation			absolute or relative path to resource, depending on your protocol. If not needed, enter an empty string. Can be a complete URL.
		 * @param array|null $queryParams		key->value array of query parameters to add to the location.
		 * @param string|null $strAnchor		anchor to add to the url
		 * @param string|null $strScheme		protocol if specifying a resource outside of the current server (i.e. http)
		 * @param string|null $strHost			server that the resource is on. Required if specifying a scheme.
		 * @param string|null $strUser			user name if needed. Some protocols like mailto and ftp need this
		 * @param string|null $strPassword		password if needed. Note that password is sent in the clear.
		 * @param string|null $intPort			port if different from default
		 * @return string
		 */
		public static function MakeUrl ($strLocation, $queryParams = null, $strAnchor = null, $strScheme = null, $strHost = null, $strUser = null, $strPassword = null, $intPort = null) {
			// Decompose
			if ($strLocation) {
				$params = parse_url($strLocation);
			}

			if (!empty($strLocation) && isset($params['path'])) {
				$strUrl = $params['path'];
			} else {
				$strUrl = '';
			}

			if (isset($params['query'])) {
				parse_str($params['query'], $queryParams2);
				if ($queryParams) {
					$queryParams = array_merge($queryParams2, $queryParams);
				} else {
					$queryParams = $queryParams2;
				}
			}

			if (empty($strAnchor) && isset($params['fragment'])) {
				$strAnchor = $params['fragment'];
			}

			if (empty($strScheme) && isset($params['scheme'])) {
				$strScheme = $params['scheme'];
			}

			if (empty($strHost) && isset($params['host'])) {
				$strHost = $params['host'];
			}

			if (empty($strUser) && isset($params['user'])) {
				$strUser = $params['user'];
			}
			if (empty($strPassword) && isset($params['pass'])) {
				$strPassword = $params['pass'];
			}
			if (empty($intPort) && isset($params['port'])) {
				$intPort = $params['port'];
			}

			if ($queryParams)  {
				$strUrl .= '?' . http_build_query($queryParams);
			}
			if ($strAnchor) {
				$strUrl .= '#' . urlencode($strAnchor);
			}

			// More complex URLs. Once you specify protocol, you will need to specify the server too.
			if ($strScheme) {
				assert(!empty($strHost));

				// We do not do any checking at this point since URLs can be complex. It is up to you to build a correct URL.
				// If you use a protocol that expects an absolute path, you must start with a slash (http), or a relative path (mailto), leave the slash off.

				// Build server portion.
				if ($intPort) {
					$strHost .= ':' . $intPort;
				}
				if ($strUser) {
					$strUser = rawurlencode($strUser);
					if ($strPassword) {
						$strUser = $strUser . ':' . rawurlencode($strPassword);
					}
					$strHost = $strUser . '@' . $strHost;
				}
				$strUrl = $strScheme . $strHost . $strUrl;
			}
			return $strUrl;
		}

		/**
		 * Returns a MailTo url.
		 *
		 * @param string $strUser
		 * @param string| null $strServer optional server. If missing, will assume server and "@" are already in strUser
		 * @param array|null $queryParams
		 * @param string|null $strName Optional name to associate with the email address. Some email clients will show this instead of the address.
		 * @return string	The mailto url.
		 */
		public static function MailToUrl ($strUser, $strServer = null, $queryParams = null, $strName = null) {
			if ($strServer) {
				$strUrl = $strUser . '@' . $strServer;
			} else {
				$strUrl = $strUser;
			}
			if ($strName) {
				$strUrl = '"' . $strName . '"' . '<' . $strUrl . '>';
			}
			$strUrl = rawurlencode($strUrl);
			if ($queryParams) {
				$strUrl .= '?' . http_build_query($queryParams, null, null, PHP_QUERY_RFC3986);
			}
			return $strUrl;
		}

		/**
		 * Utility function to create a link, i.e. an "a" tag.
		 *
		 * @param string $strUrl URL to link to. Use MakeUrl or MailToUrl to create the URL.
		 * @param string $strText The inner text. This WILL be escaped.
		 * @param array $attributes Other html attributes to include in the tag
		 * @param boolean $blnHtmlEntities False to prevent encoding
		 */
		public static function RenderLink ($strUrl, $strText, $attributes = null, $blnHtmlEntities = true) {
			$attributes["href"] = $strUrl;
			if ($blnHtmlEntities) {
				$strText = QApplication::HtmlEntities($strText);
			}
			return self::RenderTag("a", $attributes, $strText);
		}

		/**
		 * Renders a PHP string as HTML text. Makes sure special characters are encoded, and <br /> tags are substituted
		 * for newlines.
		 * @param $strText
		 */
		public static function RenderString($strText) {
			return nl2br(htmlspecialchars($strText, ENT_COMPAT | ENT_HTML5, QApplication::$EncodingType));
		}

		/**
		 * A quick way to render an HTML table from an array of data. For more control, or to automatically render
		 * data that may change, see QHtmlTable and its subclasses.
		 *
		 * Example:
		 * $data = [
		 * 				['name'=>'apple', 'type'=>'fruit'],
		 * 				['name'=>'carrot', 'type'=>'vegetable']
		 * 	];
		 *
		 * 	print(QHtml::RenderTable($data, ['name','type'], ['class'=>'mytable'], ['Name', 'Type']);
		 *
		 *
		 * @param []mixed			$data				An array of objects, or an array of arrays
		 * @param []string|null 	$strFields			An array of fields to display from the data. If the data contains objects,
		 * 												the fields will be accessed using $obj->$strFieldName. If an array of arrays,
		 * 												it will be accessed using $obj[$strFieldName]. If no fields specified, it will
		 * 												treat the data as an array of arrays and just create cells for whatever it finds.
		 * @param array|null 		$attributes			Optional array of attributes to be inserted into the table tag (like a class or id).
		 * @param []string|null 	$strHeaderTitles	Optional array of titles to be added as a header row.
		 * @param int 				$intHeaderColumnCount	Optional count of the number of columns on the left that will be
		 * 													rendered using a 'th' tag instead of a 'td' tag.
		 * @param bool 				$blnHtmlEntities	True (default) to run all titles and text through the HTMLEntities renderer. Set this to
		 * 												false if you are trying to display raw html.
		 * @return string
		 */
		public static function RenderTable(array $data, $strFields = null, $attributes = null, $strHeaderTitles = null, $intHeaderColumnCount = 0, $blnHtmlEntities = true) {
			if (!$data) {
				return '';
			}

			$strHeader = '';
			if ($strHeaderTitles) {
				foreach ($strHeaderTitles as $strHeaderTitle) {
					if ($blnHtmlEntities) {
						$strHeaderTitle = QApplication::HtmlEntities($strHeaderTitle);
					}
					$strHeader .= '<th>' . $strHeaderTitle . '</th>';
				}
				$strHeader = '<thead><tr>' . $strHeader . '</tr></thead>';
			}
			$strBody = '';
			foreach ($data as $row) {
				$intFieldNum = 0;
				$strRow = '';
				if ($strFields) {
					foreach ($strFields as $strField) {
						$intFieldNum ++;
						$strItem = '';
						if (is_object($row)) {
							$strItem = $row->$strField;
						} elseif (isset($row[$strField])) {
							$strItem = $row[$strField];
						}
						if ($blnHtmlEntities) {
							$strItem = QApplication::HtmlEntities($strItem);
						}
						if ($intFieldNum <= $intHeaderColumnCount) {
							$strRow .= '<th>' . $strItem . '</th>';
						} else {
							$strRow .= '<td>' . $strItem . '</td>';
						}
					}
				} else {
					foreach ($row as $strItem) {
						$intFieldNum ++;
						if ($blnHtmlEntities) {
							$strItem = QApplication::HtmlEntities($strItem);
						}
						if ($intFieldNum <= $intHeaderColumnCount) {
							$strRow .= '<th>' . $strItem . '</th>';
						} else {
							$strRow .= '<td>' . $strItem . '</td>';
						}
					}
				}
				$strRow = '<tr>' . $strRow . '</tr>';
				$strBody .= $strRow;
			}
			$strBody = '<tbody>' . $strBody . '</tbody>';
            $strTable = self::RenderTag('table', $attributes , $strHeader . $strBody);
			return $strTable;
		}

	}
