<?php

// TODO: the checkbox column is used pretty heavily. it would be nice to automagically create the javascript necessary for this field

/**
 * get_paged_table() - build a self contained, self managing paged table based on a select statement and miscellaneous options
 * @param string $select_statement - the GENERIC select statement to build a table with (no limit or order by specified here - those must be specified within the options array)
 * @param array $bind_array - if your select statement requires binding elements, they go here (direct relation to $db->bind_array())
 * @param array $options - the options used to configure the paging options and the display/feel of the table. see bottom of pagination.inc.php for details
 * @return string - the full html of the table
 */
function get_paged_table($select_statement, $bind_array, $options) {

	global $db;

	// make sure this is a select statement
	$test_select = ltrim(strtoupper($select_statement));
	if (substr($test_select, 0, 6) != 'SELECT')
		return '';

	// make sure there is no unquoted semicolons
	if (phelp_has_unquoted_semicolons($select_statement))
		return '';

	// do we even have any options?
	if (empty($options))
		return '';

	// do we even have a columns element to our options?
	if (!array_key_exists('columns', $options))
		return '';

	// are there more than 0 columns?
	if (count($options['columns']) == 0)
		return '';

	// reset bind array if its empty
	if (empty($bind_array))
		$bind_array = array();

	// get any override options
	$static_limit = isset($options['static_limit']) ? $options['static_limit'] : -1;
	$display_footer = (isset($options['display_footer']) && !$options['display_footer']) ? false : true;

	// should we set some default pagination for this table?
	phelp_set_default_pagination($select_statement, $options);

	// encode the table data!
	$table_data = phelp_tabledata_encode($select_statement, $bind_array, $options);

	// now we need to get the rowcount of the query
	// TODO: this is not a good way to do this
	// we really need a SELECT count() - but this would break any potential complex queries
	$rows = $db->exec_query($select_statement, $bind_array);
	if (!is_array($rows))
		return '';
	$row_count = count($rows);

	// now we get the stored pagination user_meta (and override where applicable)
	$pagination = phelp_get_pagination($select_statement, $static_limit);

	// prepare some data for the pagination div
	$selected = ' selected="selected"';
	$disabled = ' disabled="disabled"';
	$num_pages = 1;
	$current_page = 1;
	if ($pagination['limit'] > 0) {
		$num_pages = ceil($row_count / $pagination['limit']);
		$current_page = ceil($pagination['offset'] / $pagination['limit']) + 1;
	}
	if ($num_pages <= 0)
		$num_pages = 1;

	// which buttons are we going to disable?
	$btn_first_disabled = $current_page == 1 ? $disabled : '';
	$btn_prev_disabled =  $current_page == 1 ? $disabled : '';
	$btn_next_disabled =  $current_page == $num_pages ? $disabled : '';
	$btn_last_disabled =  $current_page == $num_pages ? $disabled : '';
	$btn_jump_disabled =  $num_pages == 1 ? $disabled : '';

	// adjust the select statement
	$select_statement = phelp_append_pagination_clauses($select_statement, $pagination);

	// execute the adjusted query and check to make sure we got some data back!
	$rows = $db->exec_query($select_statement, $bind_array);
	if (!is_array($rows))
		return '';

	// manage all this craziness with a random string
	$div_id = random_string(24);

	$pagination_div = '';
	$script = '';

	// build the pagination div
	if (empty($options['ignore_pagination_div'])) {

		$pagination_div = '<div class="ajax-pagination">';
		$pagination_div .= '<button class="btn btn-xs btn-default tt-bindt first-page" title="" data-original-title="First Page" data-which="first"' . $btn_first_disabled . '><i class="fa fa-fast-backward"></i></button>';
		$pagination_div .= '<button class="btn btn-xs btn-default tt-bindt previous-page" title="" data-original-title="Previous Page" data-which="prev"' . $btn_prev_disabled . '><i class="fa fa-chevron-left l"></i></button>';
		$pagination_div .= "<span style='margin: 0 10px;'> Page {$current_page} of {$num_pages} </span>";
		$pagination_div .= '<button class="btn btn-xs btn-default tt-bindt next-page" title="" data-original-title="Next Page" data-which="next"' . $btn_next_disabled . '><i class="fa fa-chevron-right r"></i></button>';
		$pagination_div .= '<button class="btn btn-xs btn-default tt-bindt last-page" title="" data-original-title="Last Page" data-which="last"' . $btn_last_disabled . '><i class="fa fa-fast-forward"></i></button>';
		if ($static_limit < 0) {
			$pagination_div .= 
				'<select class="form-control condensed num-records tt-bindt" data-original-title="Records per Page" data-which="records">'  .
	            	'<option value="5"' 	. ($pagination['limit'] == 5 ? $selected : '') 		. '>5 Per Page</option>'  .
	            	'<option value="10"' 	. ($pagination['limit'] == 10 ? $selected : '') 	. '>10 Per Page</option>'  .
	            	'<option value="25"' 	. ($pagination['limit'] == 25 ? $selected : '') 	. '>25 Per Page</option>'  .
	            	'<option value="50"' 	. ($pagination['limit'] == 50 ? $selected : '') 	. '>50 Per Page</option>'  .
	            	'<option value="100"' 	. ($pagination['limit'] == 100 ? $selected : '') 	. '>100 Per Page</option>'  .
		        	'<option value="0"' 	. ($pagination['limit'] == 0 ? $selected : '') 		. '>No limit</option>'  .
		        '</select>';
		}
	    $pagination_div .= '<input type="text" class="form-control condensed tt-bindt jump-to" data-original-title="Page Number"' . $btn_jump_disabled . ' />';
	    $pagination_div .= '<button class="btn btn-xs btn-default tt-bindt jump" title="" data-original-title="Jump to Page" data-which="jump"' . $btn_jump_disabled . '><i class="fa fa-chevron-circle-right fa-12"></i></button>';
	    $pagination_div .= '<button class="btn btn-xs btn-default tt-bindt reset" title="" data-original-title="Reset" data-which="reset"><i class="fa fa-refresh fa-12"></i></button>';
	    $pagination_div .= '</div>';

	    // if pretty print is on we should indent and newline some stuff
		if (!empty($options['pretty_print']))
			$pagination_div = str_replace(	
				array("<div", "</div>", "<button", "</button", "<i class", "<input", "<select", "</select", "<option"), 
				array("\n\t<div", "\n\t</div>\n", "\n\t\t<button", "\n\t\t</button", "\n\t\t\t<i class", "\n\t\t<input", "\n\t\t<select", "\n\t\t</select", "\n\t\t\t<option"), 
				$pagination_div);

		// we keep the javascript here so that the paged table is as self contained as possible (no extra includes!)
	    $script = <<<SCRIPT

	    <script>

	    function pagination_ajax_opts(datawhich, rowcount, additional) {

		    var optsarr = {
		    	"table_data": "{$table_data}",
		    	"which": datawhich,
		    	"rowcount": rowcount,
		    	"additional": additional
		    }

		    return array2json(optsarr);
	    }

	    $(function() {

	    	// pagination buttons
	    	$('#{$div_id} .ajax-pagination button').click(function() {

	    		var additional = 0;
	    		if ($(this).data('which') == 'jump') {
	    			additional = $(this).parent().find('.jump-to').val();
	    		}

	    		get_ajax_data_html('paged_table', pagination_ajax_opts($(this).data('which'), {$row_count}, additional), $('#{$div_id}'));
	    	});

			// did they press enter while they were in the jump to input?
			$('#{$div_id} .jump-to').keyup(function(e) {
				if (e.keyCode == 13) {
					$(this).parent().find('.jump').click();
				}
			});

			// pagination select (records per page)
			$('#{$div_id} .ajax-pagination select').change(function() {

	    		get_ajax_data_html('paged_table', pagination_ajax_opts($(this).data('which'), {$row_count}, $(this).val()), $('#{$div_id}'));
			});

			// sortable columns
			$('#{$div_id} table tr th span.sort').parent('th').addClass('sort').click(function() {

	    		get_ajax_data_html('paged_table', pagination_ajax_opts('sort', 0, $(this).find('span.sort').data('column')), $('#{$div_id}'));
			});
		});

	    </script>

SCRIPT;
	} // if empty(ignore_pagination)

	// get anything necessary to add to the html tags
	// this looks ugly, but its a lot better than the alternative

	$pre_head_pagination_div_html = html_raw($options['pre_head_pagination_div_html']);
	$post_head_pagination_div_html = html_raw($options['post_head_pagination_div_html']);
	$pre_table_html = html_raw($options['pre_table_html']);
	$table_tag = html_attr('id', $options['table_id']) 				. html_attr('class', $options['table_class']) 	. html_raw($options['table_html']);
	$pre_thead_html = html_raw($options['pre_thead_html']);
	$thead_tag = html_attr('id', $options['thead_id']) 				. html_attr('class', $options['thead_class']) 	. html_raw($options['thead_html']);
	$thead_tr_tag = html_attr('id', $options['thead_tr_id']) 		. html_attr('class', $options['thead_tr_class']). html_raw($options['thead_tr_html']);
	$thead_th_tag = html_attr('class', $options['thead_th_class'])	. html_raw($options['thead_th_html']);
	$post_thead_html = html_raw($options['post_thead_html']);
	$pre_tbody_html = html_raw($options['pre_tbody_html']);
	$tbody_tag = html_attr('id', $options['tbody_id']) 				. html_attr('class', $options['tbody_class']) 	. html_raw($options['tbody_html']);
	$tbody_tr_tag = html_attr('class', $options['tbody_tr_class'])	. html_raw($options['tbody_tr_html']);
	$tbody_td_tag = html_attr('class', $options['tbody_td_class'])	. html_raw($options['tbody_td_html']);
	$post_tbody_html = html_raw($options['post_tbody_html']);
	$pre_tfoot_html = html_raw($options['pre_tfoot_html']);
	$tfoot_tag = html_attr('id', $options['tfoot_id']) 				. html_attr('class', $options['tfoot_class']) 	. html_raw($options['tfoot_html']);
	$tfoot_tr_tag = html_attr('class', $options['tfoot_tr_class'])	. html_raw($options['tfoot_tr_html']);
	$tfoot_th_tag = html_attr('class', $options['tfoot_th_class'])	. html_raw($options['tfoot_th_html']);
	$post_tfoot_html = html_raw($options['post_tfoot_html']);
	$post_table_html = html_raw($options['post_table_html']);
	$pre_foot_pagination_div_html = html_raw($options['pre_foot_pagination_div_html']);
	$post_foot_pagination_div_html = html_raw($options['post_foot_pagination_div_html']);

	// start building the table
	$table = "\n\n<div id=\"{$div_id}\">";
	$table .= "{$script}\n";
	$table .= "{$pre_head_pagination_div_html}\n";
	$table .= "{$pagination_div}\n";
	$table .= "{$post_head_pagination_div_html}\n";
	$table .= "\n{$pre_table_html}\n";
	$table .= 	"<table {$table_tag}>";
	$table .=		"\n{$pre_thead_html}\n";
	$table .= 		"<thead {$thead_tag}>";
	$table .= 			"<tr {$thead_tr_tag}>";

	// build headers/footers with column data
	$columns = $options['columns'];
	foreach ($columns as $column_name => $column) {

		// default to show nothing
		$th = '';

		// if th is defined, use that!
		if (!empty($column['th']))
			$th = $column['th'];

		// if this is a sortable column, we need to add some additional html for the ajax calls
		if (isset($column['sortable']) && $column['sortable']) {

			$sort_class = 'sort ';
			if (!empty($options['sort_class']))
				$sort_class .= $options['sort_class'];

			$th_tmp = "<span class=\"{$sort_class}\" data-column=\"{$column_name}\">";

			// if this is the currently sorted column, then we need to display the proper icon.
			if (isset($pagination['orderby_column']) && $pagination['orderby_column'] == $column_name) {

				if (!isset($pagination['orderby_order']) || $pagination['orderby_order'] == 'ASC') {

					$th_tmp .= "<i class=\"fa fa-long-arrow-down fa-16\"></i> ";

				} else if ($pagination['orderby_order'] == 'DESC') {

					$th_tmp .= "<i class=\"fa fa-long-arrow-up fa-16\"></i> ";
				}
			}

			$th = "{$th_tmp}{$th}</span>";
		}

		// if this column has a width specified, we need to edit the width
		$this_thead_th_tag = $thead_th_tag;
		if (!empty($column['width'])) {

			$width = $column['width'];

			// lets make sure it is in the proper format and default to percentage
			if (strpos($width, 'px') === false && strpos($width, '%') === false) {
				$width = intval($width) . '%';
			}

			// now see if there is already a style attribute already set for the th
			$th_tag_style_attr_pos = strpos($this_thead_th_tag, "style=");
			if ($th_tag_style_attr_pos === false) {

				// if not, that makes this pretty easy
				$this_thead_th_tag .= " style=\"width: {$width};\" ";

			} else {

				// if DOES exist, then we gotta do some processing to get our width in there
				$this_thead_th_tag = preg_replace("/width:.*?;/", "", $this_thead_th_tag);
				$this_thead_th_tag = preg_replace("/(style=\".*?)\"/", "$1; width: {$width};\"", $this_thead_th_tag);

				$this_thead_th_tag = str_replace('";', '"', $this_thead_th_tag);
				$this_thead_th_tag = str_replace(';;', ';', $this_thead_th_tag);
			}
		}


		// build this th, and maybe the corresponding one for the footer
		$table .= "<th {$this_thead_th_tag}>{$th}</th>";

		if ($display_footer)
			$footer .= "<th {$tfoot_th_tag}>{$th}</th>";
	}

	$table .= 			"</tr>";
	$table .= 		"</thead>";
	$table .=		"\n{$post_thead_html}\n";
	$table .=		"\n{$pre_tbody_html}\n";
	$table .= 		"<tbody {$tbody_tag}>";

	// now loop through the real data (from our query) and build rows/columns
	foreach ($rows as $row) {

		$table .=		"<tr {$tbody_tr_tag}>";

		foreach ($columns as $id => $column) {

			// default to nothing
			$td = '';

			// check if a defined column matches this rows field name
			if (array_key_exists($id, $row))
				$td = $row[$id];

			// if there is a defined td key, we put that in place of the sql data
			// this allows for replace_macros to be used (and transform if so desired)
			if (!empty($column['td']))
				$td = $column['td'];

			// attempt to replace row macros
			if (array_key_exists('replace_macros', $column) && $column['replace_macros'])
				$td = phelp_replace_row_macros($td, $row);

			// attempt to use the defined transform callback
			if (array_key_exists('transform', $column) && function_exists($column['transform']))
				$td = $column['transform']($td, $row);

			// we overwrite all of that if eval is set, in which case we eval what was set and put it in td
			// do some extremely basic sanity testing here (make sure there is a return keyword in the eval statement, otherwise nothing would happen)
			if (array_key_exists('eval', $column) && strpos($column['eval'], 'return ') !== false) {

				// we replace macros in eval regardless of whether the user specified or not
				$td_tmp = phelp_replace_row_macros($column['eval'], $row);

				// as long as user input is never passed here directly this is safe
				$td_tmp = eval($td_tmp);
				if (!empty($td_tmp))
					$td = $td_tmp;
			}

			// build the td
			$table .=		"<td {$tbody_td_tag}>";
			$table .=			"{$td}";
			$table .=		"</td>";
		}

		$table .=		"</tr>";
	}

	$table .= 		"</tbody>";
	$table .=		"\n{$post_tbody_html}\n";

	// default is to display the footer
	if ($display_footer) {
		$table .=	"\n{$pre_tfoot_html}\n";
		$table .=	"<tfoot {$tfoot_tag}>";
		$table .=		"<tr {$tfoot_tr_tag}>";
		$table .=			"{$footer}";
		$table .=		"</tr>";
		$table .=	"</tfoot>";
		$table .=	"\n{$post_tfoot_html}\n";
	}

	$table .= 	"</table>";
	$table .= "\n{$post_table_html}\n";
	$table .= "{$pre_foot_pagination_div_html}\n";
	$table .= "{$pagination_div}";
	$table .= "{$post_foot_pagination_div_html}\n";

	// did we specify a with_selected?
	if (!empty($options['with_selected']))
		$table .= pagination_get_with_selected($options['with_selected']);

	$table .= "</div>\n\n";

	// should we attempt some line breaks and tabs?
	if (!empty($options['pretty_print'])) {

		// indent the thead, tbody, tfoot
		$table = str_replace(	
			array("<table", "<thead", "<tbody", "<tfoot", "</thead>", "</tbody>", "</tfoot>", "</table>"), 
			array("\n\t<table", "\n\t\t<thead", "\n\t\t<tbody", "\n\t\t<tfoot", "\n\t\t</thead>", "\n\t\t</tbody>", "\n\t\t</tfoot>", "\n\t</table>\n"),
			$table);

		// indent the trs
		$table = str_replace(	
			array("<tr", "</tr>"), 
			array("\n\t\t\t<tr", "\n\t\t\t</tr>"), 
			$table);

		// indent the tds and ths
		$table = str_replace(	
			array("<td ", "<th ", "</td>", "</th>"), 
			array("\n\t\t\t\t<td ", "\n\t\t\t\t<th ", "\n\t\t\t\t</td>", "\n\t\t\t\t</th>"), 
			$table);

		// indent all the stuff between the tds and ths
		$table = preg_replace("/(<td.*?>)(.*?)(<\/td>)/s", "$1\n\t\t\t\t\t$2$3", $table);
		$table = preg_replace("/(<th.*?>)(.*?)(<\/th>)/s", "$1\n\t\t\t\t\t$2$3", $table);
	}

	return $table;

}


/**
 * phelp_tabledata_encode() - used to encode data to pass via ajax on pagination div actions
 * NOTE: all of these params directly relate to the parameters passed to the get_paged_table() function
 * @param string $select_statement - the GENERIC select statement to build a table with (no limit or order by specified here - those must be specified within the options array)
 * @param array $bind_array - if your select statement requires binding elements, they go here (direct relation to $db->bind_array())
 * @param array $options - the options used to configure the paging options and the display/feel of the table. see bottom of pagination.inc.php for details
 * @return string a base64 encoded serialized array containing all of the parameters
 */
function phelp_tabledata_encode($select_statement, $bind_array, $options) {

	return base64_encode(serialize(array(
		's' => $select_statement,
		'b' => $bind_array,
		'o' => $options
		)));
}


/**
 * phelp_tabledata_decode()
 * @param string $str - the base64 serialized data you wish to decode
 * @param ref string $select_statement - the variable specified here will be populated with the select_statement
 * @param ref array $bind_array - the variable specified here will be populated with the bind_array
 * @param ref array $options - the variable specified here will be populated with the options array
 */
function phelp_tabledata_decode($str, &$select_statement, &$bind_array, &$options) {

	$decoded = unserialize(base64_decode($str));

	$select_statement = isset($decoded['s']) ? $decoded['s'] : '';
	$bind_array = isset($decoded['b']) ? $decoded['b'] : '';
	$options = isset($decoded['o']) ? $decoded['o'] : '';
}


/**
 * phelp_replace_row_macros() - attempt to replace substrs like %FIELD% with $macro_array['FIELD'] 
 * @param string $str a string containing macros in the form of %macro%
 * @param array $macro_array an array [hopefully] containing fields that correspond to the macros specified
 * @return string a string with all possible macros replaced
 */
function phelp_replace_row_macros($str, $macro_array) {

	$offset = 0;
	$str_len = strlen($str);
	$count = 0;

	while (true) {

		$first_percent_occurence = strpos($str, '%', $offset);
		if ($first_percent_occurence === false)
			break;

		if (++$first_percent_occurence >= $str_len)
			break;

		$second_percent_occurence = strpos($str, '%', $first_percent_occurence);
		if ($second_percent_occurence === false)
			break;

		$macro = substr($str, $first_percent_occurence, $second_percent_occurence - $first_percent_occurence);

		$adjust_str_len = 0;
		if (isset($macro_array[$macro])) {

			$adjust_str_len = strlen("%{$macro}%") - strlen($macro_array[$macro]);
			$str_len -= $adjust_str_len;

			// did some perftests with the following, and str_replace was a pretty clear winner:	-bh
			// $str = substr($str, 0, $first_percent_occurence-1) . $macro_array[$macro] . substr($str, $second_percent_occurence + 1);
			// $str = preg_replace("/%{$macro}%/", $macro_array[$macro], $str);
			$macro_array[$macro] = htmlentities($macro_array[$macro], ENT_QUOTES);
			$str = str_replace("%{$macro}%", $macro_array[$macro], $str);
		}

		$offset = $second_percent_occurence + 1 - $adjust_str_len;

		if ($offset >= $str_len)
			break;
	}

	return $str;
}


/**
 * phelp_table_hash() - simple reusable hash to [hopefully] prevent collisions - this is used for getting/setting usermeta relating to paged table information
 * @param string $str the string you wish to hash
 * @return string hashed value
 */
function phelp_table_hash($str) {

	return md5($str) . md5(strrev($str));
}


/**
 * phelp_get_pagination_user_meta() - a get_user_meta wrapper function specifically for paged_tables (manages hash key, etc.)
 * @param string $select_statement the specified select statement you wish to pull usermeta for
 * @return array the usermeta you've requested
 */
function phelp_get_pagination_user_meta($select_statement) {

	return unserialize(
				get_user_meta(
					phelp_table_hash($select_statement)
				)
			);
}


/**
 *
 * phelp_set_pagination_user_meta() - a set_user_meta wrapper function specifically for paged_tables (manages hash key, etc.)
 * @param string $select_statement the specified select statement you wish to set usermeta for
 * @param array $meta_array the array you wish to set as the usermeta
 */
function phelp_set_pagination_user_meta($select_statement, $meta_array) {

	set_user_meta(phelp_table_hash($select_statement), serialize($meta_array));
}


/**
 * phelp_get_pagination() - get the current pagination data (what was the last page user left off on? how many records? have they viewed/set any options for this table yet?)
 * @param string $select_statement - this is used to determine the paging information
 * @param optional int $static_limit - if you've overrode the default/user set limits in your code, that must be passed here
 * @param array - the pagination array
 */
function phelp_get_pagination($select_statement, $static_limit = -1) {

	$pagination = phelp_get_pagination_user_meta($select_statement);

	if (empty($pagination))
		$pagination = phelp_get_pagination_user_meta("defaultpagination:{$select_statement}");

	// do we have a static override?
	$pagination['limit'] = $static_limit < 0 ? $pagination['limit'] : $static_limit;

	$pagination = phelp_sanitize_pagination($pagination);

	return $pagination;
}


/**
 * phelp_has_unquoted_semicolons() - check for unquoted semicolons. useful for checking queries to avoid potential sql injection
 * @param string $str - the string to check for
 * @return bool true if there are unquoted semicolons, false if not
 */
function phelp_has_unquoted_semicolons($str) {

	$inside_quotes = false;
	$which_quote = '';
	for ($i = 0; $i < strlen($str); $i++) {

		if (($str[$i] == '"' || $str[$i] == "'") && $which_quote == '') {
			$inside_quotes = true;
			$which_quote = $str[$i];
		}

		if ($inside_quotes)
			continue;

		if ($str[$i] == $which_quote) {
			$inside_quotes = false;
			$which_quote = '';
		}

		if ($str[$i] == ';')
			return true;
	}

}


/**
 * phelp_set_default_pagination() - set the pagination defaults if not already set
 * @param string $select_statement - the identifier for the usermeta
 * @param ref array $options - the options used to build the paged table (we pull the default info from here)
 */
function phelp_set_default_pagination($select_statement, &$options) {

	$defaults = phelp_get_pagination_user_meta("defaultpagination:{$select_statement}");
	if (empty($defaults)) {
		$defaults = phelp_default_pagination_array();
	}

	if (isset($options['default_pagination']['offset']))
		$defaults['offset'] = $options['default_pagination']['offset'];

	if (isset($options['default_pagination']['limit']))
		$defaults['limit'] = $options['default_pagination']['limit'];

	if (isset($options['default_pagination']['orderby_column']))
		$defaults['orderby_column'] = $options['default_pagination']['orderby_column'];

	if (isset($options['default_pagination']['orderby_order']))
		$defaults['orderby_order'] = $options['default_pagination']['orderby_order'];

	$defaults = phelp_sanitize_pagination($defaults);

	phelp_set_pagination_user_meta("defaultpagination:{$select_statement}", $defaults);
}


/**
 * phelp_default_pagination_array() - if no defaults are specified, these are what all paginated tables will default to
 * @return array default pagination array
 */
function phelp_default_pagination_array() {
	return array(
		'offset' => 0,
		'limit' => 10,
		'orderby_column' => '',
		'orderby_order' => '',
		);
}


/**
 * phelp_sanitize_pagination() - ensure that a pagination array has sane values
 * @param array $pagination_array - the array to sanitize
 * @return array - will return a (mostly) sanitized pagination array [we cant check the column name here since we dont have a table at this point]
 */
function phelp_sanitize_pagination($pagination_array) {

	// if we have nothing or its not an array return the default array
	if (empty($pagination_array) || !is_array($pagination_array)) {
		return phelp_default_pagination_array();
	}

	$offset = intval(isset($pagination_array['offset']) ? $pagination_array['offset'] : 0);
	$limit = intval(isset($pagination_array['limit']) ? $pagination_array['limit'] : 0);
	$orderby_order = strtoupper(isset($pagination_array['orderby_order']) ? $pagination_array['orderby_order'] : '');

	if (!isset($offset) || !is_numeric($offset) || $offset < 0)
		$offset = 0;

	if (!isset($limit) || !is_numeric($limit) || $limit < 0)
		$limit = 10;

	if ($orderby_order != 'ASC' && $orderby_order != 'DESC')
		$orderby_order = '';

	// the offset needs to be a number divisible by the limit or we get weird errors
	// this happens a lot if you switch between records per page, etc.
	if ($offset > 0) {
		if ($offset % $limit !== 0) {
			$offset = floor($offset / $limit) * $limit;
		}
	}

	$pagination_array['offset'] = $offset;
	$pagination_array['limit'] = $limit;
	$pagination_array['orderby_order'] = $orderby_order;

	return $pagination_array;
}


/**
 * phelp_append_pagination_clauses() - where the magic happens - appending any possible sql clauses
 * @param string $select_statement - the sql statement to append clauses to
 * @param array $pagination_array - the array containing the pagination data
 * @param int $row_count - necessary for some cases of determining LIMIT clause
 * @return string - the amended [sql statement] string with clauses
 */
function phelp_append_pagination_clauses($select_statement, $pagination_array, $row_count = 0) {

	//todo: check everything out here. there are a lot of vulnerabilities as is

	global $db;

	$clauses = '';

	if (!empty($pagination_array['orderby_column'])) {

		if (is_valid_column_of_query($pagination_array['orderby_column'], $select_statement))
			$clauses .= " ORDER BY {$pagination_array['orderby_column']} {$pagination_array['orderby_order']}";
	}

	if ($pagination_array['limit'] > 0) {
		$clauses .= " LIMIT {$pagination_array['offset']}, {$pagination_array['limit']}";
	} else {
		if ($row_count > 0) {
			$clauses .= " LIMIT 0, {$row_count}";
		}
	}
	
	$select_statement .= $clauses;

	return $select_statement;
}


/**
 * pagination_get_with_selected() - get the little "With Selected: [buttons]" div
 * @param array $buttons - the array of button data to build
 * accepted key/values:
 * $buttons['img'] string REQUIRED - the img src for the button
 * $buttons['type'] string - defaults to submit, other accepted type is button. please note there is no checking for a valid type
 * $buttons['title'] string - the tt-bind/img alt/etc. string to display
 * - all other array key/vals are treated as html attributes for the <button> element
 * @return string - the string containing the entire div
 */
function pagination_get_with_selected($buttons = array()) {

	if (!is_array($buttons))
		return '';

	$t = "\t";
	$n = "\n";

	$with_selected = 
		'<div class="with-selected">' . $n .
		$t . _('With Selected:') . $n . $n;

	foreach ($buttons as $button) {

		// invalid button
		if (!is_array($button))
			continue;

		// button defaults
		$type = grab_array_var($button, 'type', 'submit');
		$title = grab_array_var($button, 'title', 'title');
		$img = grab_array_var($button, 'img', null);

		// we need an image to build this button
		if ($img == null)
			continue;

		// get rid of these since we'll be treating $button as an attributes array now
		unset($button['img']);
		unset($button['type']);
		unset($button['title']);

		// make sure tt-bind class is present
		html_set_attribute('class', 'tt-bind', $button, $merge = true);

		// the rest of $button is essentially an attributes array for the <button> element
		$button_tag_html = html_parse_attributes($button);

		// append our button data
		$with_selected .=
			$t . "<button{$button_tag_html} title=\"{$title}\" data-original-title=\"{$title}\" name=\"multi\" type=\"{$type}\">" . $n .
			$t . $t . "<img src=\"{$img}\" border=\"0\" alt=\"{$title}\" />" . $n .
			$t . "</button>" . $n . $n;
	}

	$with_selected .= '</div>' . $n . $n;

	return $with_selected;
}


/* these are the options respected by the get_paged_table() function 
$opts = array(

	'ignore_pagination_div' => false,

	'static_limit' => 10,
	'display_footer' => false,
	'sort_class' => '',
	'pretty_print' => true,

	'default_pagination' => array(
		'offset' => 0,
		'limit' => 0,
		'orderby_column' => '',
		'orderby_order' => '',
		),

	'pre_head_pagination_div_html' => '',
	'post_head_pagination_div_html' => '',

	'pre_table_html' => '<script>$(function() { alert("pre_table_html alert"); });</script>',

	'table_id' => 'T_ID',
	'table_class' => 'T_CLASS',
	'table_html' => ' cellpadding="4" ',

	'pre_thead_html' => '',

	'thead_id' => 'THEAD_ID',
	'thead_class' => '',
	'thead_html' => '',
	'thead_tr_id' => '',
	'thead_tr_class' => '',
	'thead_tr_html' => '',
	'thead_th_class' => '',
	'thead_th_html' => '',

	'post_thead_html' => '',
	'pre_tbody_html' => '',

	'tbody_id' => '',
	'tbody_class' => '',
	'tbody_html' => '',
	'tbody_tr_class' => '',
	'tbody_tr_html' => '',
	'tbody_td_class' => '',
	'tbody_td_html' => '',

	'post_tbody_html' => '',
	'pre_tfoot_html' => '',

	'tfoot_id' => '',
	'tfoot_class' => '',
	'tfoot_html' => '',
	'tfoot_tr_id' => '',
	'tfoot_tr_class' => '',
	'tfoot_tr_html' => '',
	'tfoot_th_class' => '',
	'tfoot_th_html' => '',

	'post_tfoot_html' => '',

	'post_table_html' => '<script>$(function() { alert("post_table_html alert"); });</script>',

	'pre_foot_pagination_div_html' => '',
	'post_foot_pagination_div_html' => '',

	'columns' => array(
		'checkbox1' => array(
			'th' => 'Checkbox',
			'td' => '<input type="checkbox" name="event_id[]" value="%event_id%" />',
			'replace_macros' => true,
			),
		'event_id' => array(
			'th' =>  'Event ID',
			'sortable' => false,
			'transform' => 'boldify',
			'eval' => 'if (%event_id% == 1) { echo "foo"; } else { echo "bar"; }'
			'width' => '10px',
			), 
		'event_time' => array(
			'th' =>  'Event Time',
			'sortable' => false,
			'transform' => 'underlinify',
			'width' => '24%',
			),
		),

	'with_selected' => $buttons array mentioned in pagination_get_with_selected

	);
*/
