<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id: wikiplugin_fancytable.php 67289 2018-08-28 05:15:20Z chibaguy $

function wikiplugin_fancytable_info()
{
	$tsOn = Table_Check::isEnabled();
	if ($tsOn === true) {
		$ts = new Table_Plugin;
		$ts->createParams();
		$tsparams = $ts->params;
		unset($tsparams['server']);
	} else {
		$tsparams = [];
	}
	$params = array_merge(
		[
			 'head' => [
				 'required' => false,
				 'name' => tra('Heading Row'),
				 'description' => tr('Header rows of the table. Use %0 to separate multiple rows.', '<code>>></code>'),
				 'default' => '',
				 'since' => '1'
			 ],
			 'headclass' => [
				 'required' => false,
				 'name' => tra('Heading CSS Class'),
				 'description' => tra('CSS class to apply to the heading row.'),
				 'default' => '',
				 'since' => '1'
			 ],
			 'headaligns' => [
				 'required' => false,
				 'name' => tra('Header Horizontal Alignment'),
				 'description' => tr(
					 'Horizontal alignments for header cells, separated by %0. Choices: %1',
					 '<code>|</code>',
					 '<code>left</code>, <code>right</code>, <code>center</code>, <code>justify</code>'
				 ),
				 'default' => '',
				 'since' => '4.1',
				 'filter' => 'text',
			 ],
			 'headvaligns' => [
				 'required' => false,
				 'name' => tra('Header Vertical Alignment'),
				 'description' => tr(
					 'Vertical alignments for header cells, separated by %0". Choices: %1',
					 '<code>|</code>',
					 '<code>top</code>, <code>middle</code>, <code>bottom</code>, <code>baseline</code>'
				 ),
				 'default' => '',
				 'since' => '4.1',
				 'filter' => 'text',
			 ],
			 'colwidths' => [
				 'required' => false,
				 'name' => tra('Column Widths'),
				 'description' => tr('Column widths followed by "px" for pixels or "%" for percentages. Each column
				    separated by %0.', '<code>|</code>'),
				 'default' => '',
				 'since' => '4.1'
			 ],
			 'colaligns' => [
				 'required' => false,
				 'name' => tra('Cell Horizontal Align'),
				 'description' => tr(
					 'Table body column horizontal alignments, separated by %0. Choices: %1',
					 '<code>|</code>',
					 '<code>left</code>, <code>right</code>, <code>center</code>, <code>justify</code>'
				 ),
				 'default' => '',
				 'since' => '4.1',
				 'filter' => 'text',
			 ],
			 'colvaligns' => [
				 'required' => false,
				 'name' => tra('Cell Vertical Align'),
				 'description' => tr(
					 'Table body column vertical alignments, separated by %0. Choices: %1',
					 '<code>|</code>',
					 '<code>top</code>, <code>middle</code>, <code>bottom</code>, <code>baseline</code>'
				 ),
				 'default' => '',
				 'since' => '4.1',
				 'filter' => 'text',
			 ],
		],
		$tsparams
	);
	return [
		'name' => tra('Fancy Table'),
		'documentation' => 'PluginFancyTable',
		'description' => tra('Create a formatted table that can be filtered and sorted'),
		'prefs' => ['wikiplugin_fancytable'],
		'body' => tr(
			'Rows separated by %0 in the header; for the table body, one row per line. Cells separated by %1 (since Tiki4) or %2 in both cases.',
			'<code>>></code>',
			'<code>|</code>',
			'<code>~|~</code>'
		),
		'iconname' => 'table',
		'introduced' => 1,
		'tags' => [ 'basic' ],
		'params' => $params,
	];
}

function wikiplugin_fancytable($data, $params)
{
	global $prefs;
	$tagremove = [];
	$pluginremove = [];
	static $iFancytable = 0;
	++$iFancytable;
	extract($params, EXTR_SKIP);
	if (empty($sortable)) {
		$sortable = 'n';
	}
	$msg = '';

	if ((isset($sortable) && $sortable != 'n')) {
		if (Table_Check::isEnabled()) {
			$ts = new Table_Plugin;
			$ts->setSettings(
				'wpfancytable' . $iFancytable,
				'n',
				$sortable,
				isset($sortList) ? $sortList : null,
				isset($tsortcolumns) ? $tsortcolumns : null,
				isset($tsfilters) ? $tsfilters : null,
				isset($tsfilteroptions) ? $tsfilteroptions : null,
				isset($tspaginate) ? $tspaginate : null,
				isset($tscolselect) ? $tscolselect : null,
				null,
				null,
				isset($tstotals) ? $tstotals : null,
				isset($tstotalformat) ? $tstotalformat : null,
				isset($tstotaloptions) ? $tstotaloptions : null
			);
			if (is_array($ts->settings)) {
				$ts->settings['resizable'] = true;
				Table_Factory::build('plugin', $ts->settings);
				$sort = true;
			} else {
				$sort = false;
			}
		} else {
			$sort = false;
		}

		if ($sort === false) {
			if ($prefs['feature_jquery_tablesorter'] === 'n') {
				$msg = '<em>' . tra('The jQuery Sortable Tables feature must be activated for the sort feature to work.')
					. '</em>';
			} elseif ($prefs['javascript_enabled'] !== 'y') {
				$msg = '<em>' . tra('JavaScript must be enabled for the sort feature to work.') . '</em>';
			} else {
				$msg = '<em>' . tra('Unable to load the jQuery Sortable Tables feature.') . '</em>';
			}
		}
	} else {
		$sort = false;
	}

	//Start the table
	$style = $sort === true ? ' style="visibility:hidden"' : '';
	$wret = '<div class="table-responsive"><div id="wpfancytable' . $iFancytable . '-div"' . $style
		. ' class="ts-wrapperdiv">' . "\r\t";
	$wret .= '<table class="table table-striped table-hover normal" id="wpfancytable' . $iFancytable . '">' . "\r\t";

	//Header
	if (isset($head)) {
		//set header class
		if (! empty($headclass)) {
			$tdhdr = "\r\t\t\t" . '<th class="' . $headclass . '"';
		} else {
			$tdhdr = "\r\t\t\t<th";
		}
		//replace tiki tags, plugins and other enclosing characters with hash strings before creating table so that any
		//pipes (| or ~|~) inside aren't mistaken for cell dividers
		preprocess_section($head, $tagremove, $pluginremove);

		if ($sort) {
			$type = 'hs';
		} else {
			$type = 'h';
		}
		//now create header table rows
		$headrows = process_section(
			$head,
			$type,
			'>>',
			$tdhdr,
			'</th>',
			isset($colwidths) ? $colwidths : '',
			isset($headaligns) ? $headaligns : '',
			isset($headvaligns) ? $headvaligns : ''
		);

		//restore original tags and plugin syntax
		$headhtml = $headrows['html'];
		postprocess_section($headhtml, $tagremove, $pluginremove);

		$wret .= '<thead>' . $headhtml . "\r\t" . '</thead>' . "\r\t";
	}

	//Body
	//replace tiki tags, plugins and other enclosing characters with hash strings before creating table so that any
	//pipes (| or ~|~) inside aren't mistaken for cell dividers
	preprocess_section($data, $tagremove, $pluginremove);

	if ($sort) {
		$type = 'bs';	//sortable body rows - do not assign odd/even class to these since jquery will do it
	} else {
		$type = 'b';	//plain body rows
	}
	//now create table body rows
	$bodyrows = process_section(
		$data,
		$type,
		"\n",
		"\r\t\t\t" . '<td',
		'</td>',
		isset($colwidths) ? $colwidths : '',
		isset($colaligns) ? $colaligns : '',
		isset($colvaligns) ? $colvaligns : ''
	);

	//restore original tags and plugin syntax
	$bodyhtml = $bodyrows['html'];
	postprocess_section($bodyhtml, $tagremove, $pluginremove);

	//end the tbody
	$wret .= '<tbody>' . $bodyhtml . "\r\t" . '</tbody>';

	if (isset($ts->settings)) {
		$footer = Table_Totals::getTotalsHtml($ts->settings, $bodyrows['cols']);
		if ($footer) {
			$wret .= $footer;
		}
	}
	$wret .= "\r" . '</table></div></div>' . "\r" . $msg;
	return $wret;
}

/**
 * To keep pipes (| or ~|~) inside other Tiki tags or plugins, or other "enclosing" characters from being mistaken as
 * cell dividers, replace them with hash strings until after the table is created
 *
 * @param 		string			$data			Header or body text with tags and plugins removed
 * @param 		array			$tagremove		Key => value pairs of Hash => data for tags or enclosing characters
 * @param 		array			$pluginremove	Key => value pairs of Hash => data for plugins
 */
function preprocess_section(&$data, &$tagremove, &$pluginremove)
{
	$parserlib = TikiLib::lib('parser');
	//first replace plugins with hash strings since they may contain pipe characters
	$parserlib->plugins_remove($data, $pluginremove);

	//then replace tags or other enclosing charcters that could enclose a pipe (| or ~|~) character with a hash string
	$tikilib = TikiLib::lib('tiki');
	$tags = [
		[
			'start' => '\(\(',		// (( ))
			'end'	=> '\)\)',
		],
		[
			'start' => '\[',		// [ ]
			'end'	=> '\]',
		],
		[
			'start' => '~np~',		// ~np~ ~/np~
			'end'	=> '~\/np~',
		],
		[
			'start' => '~tc~',		// ~tc~ ~/tc~
			'end'	=> '~\/tc~',
		],
		[
			'start' => '~hc~',		// ~hc~ ~/hc~
			'end'	=> '~\/hc~',
		],
		[
			'start' => '\^',		// ^ ^
			'end'	=> '\^',
		],
		[
			'start' => '__',		// __ __
			'end'	=> '__',
		],
		[
			'start' => '\:\:',		// :: ::
			'end'	=> '\:\:',
		],
		[
			'start' => '\:\:\:',		// ::: :::
			'end'	=> '\:\:\:',
		],
		[
			'start' => '\'\'',		// '' ''
			'end'	=> '\'\'',
		],
		[
			'start' => '-\+',		// -+ +-
			'end'	=> '\+-',
		],
		[
			'start' => '-=',		// -= =-
			'end'	=> '=-',
		],
		[
			'start' => '===',		// === ===
			'end'	=> '===',
		],
		[
			'start' => '--',		// -- --
			'end'	=> '--',
		],
		[
			'start' => '\(',		// ( )
			'end'	=> '\)',
		],
		[
		'start' => '\"',		// " "
		'end'	=> '\"',
		],
	];
	$count = count($tags) - 1;
	$pattern = '/(';
	foreach ($tags as $key => $tag) {
		$pattern .= $tag['start'] . '(?:(?!' . $tag['end'] . ').)*' . $tag['end'];
		if ($key < $count) {
			$pattern .= '|';
		}
	}
	$pattern .= ')/';
	preg_match_all($pattern, $data, $matches);
	foreach ($matches[0] as $match) {
		$tagremove['key'][] = '§' . md5($tikilib->genPass()) . '§';
		$tagremove['data'][] = $match;
		$data = isset($tagremove['data']) ? str_replace($tagremove['data'], $tagremove['key'], $data) : $data;
	}
}

/**
 * Process header or body string to convert to table rows
 *
 * @param 		string			$data		Header or body string to be broken down into table rows
 * @param 		string			$type		Indicates whether rows are sortable or not, which impacts class assigned
 * @param 		string			$line_sep	Row separator (>> for header and \n for body)
 * @param 		string			$cellbeg	HTML <th or <td tag and appropriate class attribute
 * @param 		string			$cellend	HTML </th> or </td> ending tags
 * @param 		numeric			$widths		User input for column widths
 * @param 		string			$aligns		User input for horizontal text alignment within cells
 * @param 		string			$valigns	User input for vertical text alignment within cells
 *
 * @return 		string			$wret		HTML string for the header or body rows processed
 */
function process_section($data, $type, $line_sep, $cellbeg, $cellend, $widths, $aligns, $valigns)
{
	$separator = strpos($data, '~|~') === false ? '|' : '~|~';
	$lines = explode($line_sep, $data);
	$widths = ! empty($widths) ? explode('|', $widths) : '';
	$aligns = ! empty($aligns) ? explode('|', $aligns) : '';
	$valigns = ! empty($valigns) ? explode('|', $valigns) : '';
	$trbeg = "\r\t\t<tr>";
	$trend = "\r\t\t</tr>";
	$l = 0;
	$rnum1 = '';
	$rnum2 = '';
	$wret = '';
	$row_is_odd = true;
	//Each row
	foreach ($lines as $line) {
		$line = trim($line);
		if (strlen($line) > 0) {
			if ($type == 'b') {
			//	if ($row_is_odd) {
			//		$cellbeg = "\r\t\t\t" . '<td class="odd"';
			//		$row_is_odd = false;
			//	} else {
			//		$cellbeg = "\r\t\t\t" . '<td class="even"';
			//		$row_is_odd = true;
			//	}
				//don't set odd/even class if tablesorter is on because jquery will add it
				//and the classes won't alternate correctly if added here too
			} elseif ($type == 'bs') {
				$cellbeg = "\r\t\t\t" . '<td';
			}
			$c = 0;
			$row = '';
			$parts = explode($separator, $line);
			//Each column within a row
			foreach ($parts as $column) {
				$colnum = 'col' . $c;
				$colspan = '';
				$rowspan = '';
				/*
				 * Match / (colspan) or \ (rowspan) characters in whichever order at the beginning of the cell
				 * $matches[0][0] shows entire match. There are 3 strings being matched in the preg_match_all,
				 * so $matches[1][0] shows the character matched for the first string (\), $matches[2][0] the
				 * second character (/), and $matches[3][0] the third character (\)
				*/
				if (preg_match_all("/^(\\\\)*(\/)*(\\\\)*/", $column, $matches)) {
					$column = substr($column, strlen($matches[0][0]));
					//create colspan if there are / characters at beginning of cell
					if ($matches[2][0]) {
						$colspan = ' colspan="' . substr_count($matches[0][0], $matches[2][0]) . '"';
					}
					//create rowspan if there are \ characters at beginning of cell
					if ($matches[1][0] || $matches[3][0]) {
						if ($matches[1][0]) {
							$rnum1 = substr_count($matches[0][0], $matches[1][0]);
						}
						if ($matches[3][0]) {
							$rnum2 = substr_count($matches[0][0], $matches[3][0]);
						}
						$rnum = $rnum1 + $rnum2;
						$rowspan = ' rowspan="' . $rnum . '"';
						//If there's another rowspan still in force, bump up the column number
						if (isset(${$colnum}['col']) && ${$colnum}['col'] == $c) {
							if ((${$colnum}['span'] - ($l - ${$colnum}['line'])) > 0) {
								$c++;
							}
						}
						//Note the info for this new rowspan
						${$colnum}['col'] = $c;
						${$colnum}['line'] = $l;
						${$colnum}['span'] = $rnum;
					}
				}

				//set column style
				$colstyle = '';
				if (! empty($widths) || ! empty($aligns) || ! empty($valigns)) {
					//If there's another rowspan still in force, bump up the column number
					if (isset(${$colnum}['col']) && ${$colnum}['col'] == $c && ($l > ${$colnum}['line'])) {
						if ((${$colnum}['span'] - ($l - ${$colnum}['line'])) > 0) {
							$c++;
						}
					}
					$colstyle = ' style="';
					$colstyle .= ! empty($widths[$c]) ? ' width: ' . $widths[$c] . ';' : '';
					$colstyle .= ! empty($aligns[$c]) ? ' text-align: ' . $aligns[$c] . ';' : '';
					$colstyle .= ! empty($valigns[$c]) ? ' vertical-align: ' . $valigns[$c] : '';
					$colstyle .= '"';
				}
				$row .= $cellbeg . $colspan . $rowspan . $colstyle . '>' . $column . $cellend;
				$c++;//increment column number
			}
			$wret .= $trbeg . $row . $trend;
		}
		$l++;//increment row number
	}
	$ret['html'] = $wret;
	$ret['cols'] = count($parts);
	return $ret;
}

/**
 * Replace hash strings with original tag and plugin content
 *
 * @param 		string		$data			Header or body text processed into table rows with hash strings replacing
 * 												tags and plugins
 * @param 		array		$tagremove		Tag hash => data pairs for tags that were replaced with hash strings
 * @param 		array		$pluginremove	Plugin Hash => data pairs for plugins that were replaced with hash strings
 */
function postprocess_section(&$data, &$tagremove, &$pluginremove)
{
	//first restore tag strings
	$parserlib = TikiLib::lib('parser');
	if (isset($tagremove['key']) and count($tagremove['key'])
		and count($tagremove['key']) == count($tagremove['data'])) {
		$data = str_replace($tagremove['key'], $tagremove['data'], $data);
	}
	//then restore plugin strings
	$parserlib->plugins_replace($data, $pluginremove);
	//reset variables since this function is run for both the header and body
	$tagremove = [];
	$pluginremove = [];
}
