<?php
/**
 * display_list - Data structure for displaying search results. Used by result_list_view (and now many other views #$%&$#@).
 *                This will get serialized and stored in session so be careful with using objects here.
 *                This started as a nice OOP implementation but someone with less OOP skills
 *                and has no idea what encapsulation is, and made this into a big mess...
 *                In the process of rewritting the bad code...
 *
 * @author $Author: dtong $
 * @version $Id: display_list.php,v 1.44 2011/05/09 08:03:10 dtong Exp $
 * @copyright Copyright (c) 2009, Tiller Software Co., Ltd.
*/
//require_once('Utility.php');

class display_list {
	const SORT_DESC = 1;
	const SORT_ASC = 0;
	const MIN_PER_PAGE = 1;
	const MAX_PER_PAGE = 200;
	const MIN_SHOW_PAGE_NUMBERS = 0;
	const MAX_SHOW_PAGE_NUMBERS = 10;
	const FORCE_INIT_SORT_SIZE_LIMIT = 6000;
	const SECONDARY_SORT_SIZE_LIMIT = 6000;
	const SESSION_POSTFIX = 'searchsort_';
	const HIDEDATA_PREFIX = 'hide';
	const HIDEDATA_REPLACE_STR = '__DATA__';

	private $CI;
	private $data=array(); //The actual data array
	private $counter=0; //used for looping thru the array
	public $total=0; //Total number of array element, set in reset()
	public $pagetitle=''; //The title on the top of the page
	public $titles=FALSE; //Array data, the titles/names on the first row of the table
	public $widths=FALSE; //Array data, the td width
	//The divid of this result window, so we know which dhtml window to close
	//This is also used for full refresh divid and the session variable name postfix.
	//Use refresh to override the divid for full refresh. Set iswindow to false for normal div refresh.
	//When all_page is set to TRUE then this is not used much besides as the session name
	//Do not modify this more than once...
	public $divid='defaultdisplaylistdiv';
	public $closebutton=TRUE; //Display close button or not
	public $pdfbutton=TRUE; //Display PDF button or not
	public $closebutton_jstext = ''; //Add more jscript function after the close function
	public $colortrack=TRUE; //use 2 tones in the result page or not
	public $display_total=FALSE; //Overrides the total of items, if this is false then $total is used
	public $use_div_table=FALSE; //Use HTML table or div table
	public $searchid=FALSE;
	public $title_nobr=TRUE;
	public $col_align_top=TRUE;
	public $allpage=FALSE;	//show all data no paging
	public $default_col=0; //Tells the view where to put the arrow
	public $sort_columns = TRUE; //To activate column sorting or not
	public $cssclass_titlerow = '"map_restr"';
	public $cssclass_datarow1 = 'map_restr1';
	public $cssclass_datarow2 = 'map_restr2';

	public $sort_direction=self::SORT_ASC; //Desc asc actions
	//public $paging='null'; // - or + for prev and next links
	public $current_page=1; //The physcial current page
	public $perpage = 20; //Items per page. This will get reassign
	//Overrides the $this->divid for full refresh. The divid can be used as the session name and this is the actual refresh divid/window-name
	//Why are we doing this? Don't ask...
	public $refresh = FALSE; // The refresh divid

	//I added the following becuase [NAME_NOT_MENTIONED (Thank God he is gone)] had no idea about data structures and OOP and
	//they are assigned in his code (mostly admin) but not here. There are more properties assigned to this class but not defined here.
	//Have to deal with them later. It is good and bad that PHP is so flexible....
	//The following are admin_list_view related
	public $page = '';
	public $listitem=array();
	public $selectlistitem='';
	//TODO: There are many other hacks like these, have to look later

	//The following were added to make this object smarter and be able to resuse this class across the whole site
	private $datapage = array(); //The current page data that is going to be shown in this session
	private $totalpage= 0; //Total number of array element in data page, set in reset()
	//Always keep the last 2 sort columns so we can always have a secondary sort column (if this one is the same as the last first...)
	private $lastsort_cols = array(FALSE, FALSE);
	private $data_compressed = ''; //Compressed version of $data
	private $last_sort_action = NULL; //This or $last_sort_column is null means the first time running the object
	private $last_sort_column = NULL;
	//Force it to sort even if it is not suppose to
	public $force_sort = FALSE;
	//The list table divid. Have to override if more than 2 displaylists at the same time which is not likely
	//The is the div around the html table
	public $sort_refresh_divid = 'displaylist_box';
	//Generate a new unique name each time reset() is called
	public $sort_refresh_divid_unqiue = TRUE;
	//private $sort_refresh_divid_unqiue_counter = 1;
	//Is the full refresh for a window or a normal div. This applies to paging number links only.
	public $iswindow = TRUE;
	//Overrides the $this->divid for full refresh
	//public $divid_override = '';
	//The view file to load when doing a full refresh. This is the big one that has everything.
	public $view_file = '';
	//Tells the code to flip the current sort direction $sort_direction
	public $change_sort_direction = FALSE;
	//For extrenal code to put presistent storage (small size only) here. Not used yet
	//public $my_storage = array();

	//Tells the session has been saved or not
	private $session_saved = FALSE;
	private $use_compression;
	private $compression_threshold;

	//Template string for data columns that have links. These are sprintf format strings.
	private $data_template_strings = array();
	//Use customized sort function for specified columns. Contains the compare function(s).
	public $column_sort_function = array();
	//css text-align, default is left and can change to 'center' or 'right'
	public $column_data_align = array();
	//contains the columns that need the striptag function during sort/compare.
	//Index is the column and just assign the TRUE value to it
	public $column_use_striptag = array();
	//Tells to rever the data array or not
	private $isreversed = FALSE;

	function __construct() {
		//Do not assign class variable to objects here like CI since they will not be valid after loading from session
		//unless you modify the clone method, but you don't want to clone the whole CI object...
		$this->CI = & get_instance();
		$this->perpage = $this->CI->config->item('cms_max_perpage');
		$this->use_compression = $this->CI->config->item('cms_searchsession_compress');
		$this->compression_threshold = $this->CI->config->item('cms_searchsession_compress_threshold');
		osa_load_lang('mapping');
	}

	function __wakeup() {
		$this->CI = & get_instance();
		$this->change_sort_direction = FALSE;
		$this->force_sort = FALSE;
		$this->session_saved = FALSE;
		//$this->perpage = $this->CI->config->item('cms_max_perpage');
		osa_load_lang('mapping');
	}

	//Prepares the datapage[] - this is the page to be presented to the user
	//Always call this before loading the view. This is required even if you are using all_page=TRUE
	//since it is responsible to flip the sort direction. But if sorting is not required then NO NEED to call this function.
	//This is NOT required for PDF generation.
	function prepare_page() {
		$this->init_compress();

		//Make sure number of elements per page is acceptable
		if ( !osa_is_int($this->perpage) || $this->perpage < self::MIN_PER_PAGE ) {
			$this->perpage = self::MIN_PER_PAGE;
		}
		elseif ($this->perpage > self::MAX_PER_PAGE ) {
			$this->perpage = self::MAX_PER_PAGE;
		}

	  //Get total again anyway
      $this->total = count($this->data);
     //Reset the current page elements
      $this->datapage = array();
      if ($this->total <=0 ) {
      	$this->save_last_info();
         return FALSE;
      }
      if ( empty($this->default_col) || !osa_is_int($this->default_col) ) {
      	$this->default_col = 0;
      }
      $this->default_col = (int) $this->default_col;
      $skip_sort = FALSE;
      $first_time = FALSE;
	   if ( $this->last_sort_action === NULL ) {
	   	//First time so we don't want to sort again since DB result is already sorted
         $this->last_sort_action = $this->sort_direction;
         $this->last_sort_column = $this->default_col;
         $skip_sort = TRUE;
         //Tells this is the very first time running the object
         $first_time = TRUE;
      }
      elseif ( $this->change_sort_direction === TRUE ) {
      	//Not the first time so:
      	//Flip the sort action so everytime a click happens the result is different
      	if ( $this->sort_direction == self::SORT_ASC) {
		       $this->sort_direction = self::SORT_DESC;
		   }
		   else {
            $this->sort_direction = self::SORT_ASC;
		   }
		   //User clicked on a different column for the first time so sort ascending
		   if ( $this->last_sort_column != $this->default_col ) {
		   	$this->sort_direction = self::SORT_ASC;
		   }
		   //At this point we know which direction we are sorting on the very same column
	   	if ( $this->last_sort_column == $this->default_col && !$this->isreversed &&
	   		  !$this->force_sort ) {
	   		//User click on the same column so just reverse the array
	   		//Note, the stored array can be in desc or asc order depending on the sort direction when updating the array
	   		$ret = $this->prepare_page_reverse();
	   		$this->isreversed = TRUE;
	   		if ( $this->allpage ) {
	   			$this->counter = $this->total - 1;
	   		}
	   		return $ret;
	   	}
   		$this->isreversed = FALSE;
   		if ( $this->last_sort_column == $this->default_col ) {
   			$skip_sort = TRUE;
   		}
	   }
	   else {
	   	//This means paging click
	   	if ( $this->isreversed ) {
	   		$ret = $this->prepare_page_reverse();
	   		if ( $this->allpage ) {
	   			$this->counter = $this->total - 1;
	   		}
	   		return $ret;
	   	}
	   }

		//Figure out how many pages
		$total_page = $this->page_total();
      //Make sure data is correct
		if ( !osa_is_int($this->current_page) ) {
	      $this->current_page = 1;
		}

      //Make sure a valid current page number
		if ( $this->current_page <= 0 ) {
			$this->current_page = 1;
		}
		elseif ( $this->current_page > $total_page ) {
			$this->current_page = $total_page;
		}

	   $sort_cols = array($this->default_col);
	   //Add secondary sort columns if any
	   //Too slow to have secondary sort when we have alot of data
	   if ( $this->total < self::SECONDARY_SORT_SIZE_LIMIT ) {
		   if ( $this->lastsort_cols[0] !== FALSE ) {
		   	if ( $this->default_col != $this->lastsort_cols[0] ) {
		   		$sort_cols[] = $this->lastsort_cols[0];
		   	}
		   	elseif ( $this->lastsort_cols[1] !== FALSE && $this->default_col != $this->lastsort_cols[1] ) {
		   		$sort_cols[] = $this->lastsort_cols[1];
		   	}
		   }
	   }

	   if ( $first_time && !$this->force_sort ) {
	   	//We know query result is not natural sort so it is better to sort again even we don't have to
	   	//We only do this if the result is small enough
			if ( $this->total < self::FORCE_INIT_SORT_SIZE_LIMIT ) {
				$this->force_sort = TRUE;
			}
	   }

	   if ( (!$skip_sort && ($this->last_sort_action != $this->sort_direction || $this->last_sort_column != $this->default_col))
	   	  || $this->force_sort ) {
	   	$striptags = FALSE;
	   	if ( isset($this->column_use_striptag[$this->default_col]) && $this->column_use_striptag[$this->default_col] ) {
	   		$striptags = TRUE;
	   	}
	   	/*
	   	if ( $this->total < 1600 )  {
	   		//Result set is small enough to run striptag
	   		$striptags = TRUE;
	   	}
	   	else {
	   		//Larger result set so
		   	//Sample of a few data (16 max) and see can we find the common tags in search results tags
		   	//so we know to strip the tags or not. Not perfect but works most of the time.
		   	$i = 0;
		   	while (TRUE) {
					if ( !isset($this->data[$i][$this->default_col]) ) {
						break;
					}
			   	if ( stripos($this->data[$i][$this->default_col], '<a ') !== FALSE ||
			   		  stripos($this->data[$i][$this->default_col], '<font ') !== FALSE ) {
		            $striptags = TRUE;
		            break;
			   	}
			   	if ( $i > 10240 ) {
			   		break;
			   	}
			   	if ( $i < 10 ) {
			   		$i += 2;
			   	}
			   	else {
			   		$i *= 2;
			   	}
		   	} //while
	   	 } //else
	   	 */
			//We need comapre functions other than natural sort since it won't work with negative numbers etc.
	   	$compare_function = FALSE;
	   	if ( array_key_exists($this->default_col, $this->column_sort_function) ) {
	   		$compare_function = $this->column_sort_function[$this->default_col];
	   	}
		   //Always sort again since we might not save any sorted $this->data
		   //The sorting functions are optimized so do not change here. This is much faster and cleaner than the orginal code...
			if ( $this->sort_direction != self::SORT_DESC ) {
	         osa_array_natsort($this->data, $sort_cols, FALSE, FALSE, $striptags, $compare_function);
			}
			else {
	         osa_array_natsort($this->data, $sort_cols, FALSE, TRUE, $striptags, $compare_function);
			}
			//Tell destructor to update compressed data since $data changed
			$this->data_compressed = '';
	   }
      //The whole data array is sorted now and we need to find the elements for the current page
      $start_index = ($this->current_page-1) * $this->perpage;
      $end_index = $start_index + $this->perpage - 1;
      if ( $end_index >= $this->total ) {
      	$end_index = $this->total-1;
      }
	   if ( $start_index >= $this->total || $start_index > $end_index ) {
	   	//$this->paging = 'null';
         return FALSE;
      }
      for ( $i=$start_index; $i<=$end_index; $i++ ) {
         $this->datapage[] = & $this->data[$i];
      }
      $this->save_last_info();
      //$this->paging = 'null';
      return TRUE;
	}

	private function save_last_info() {
		//Save the current sort columns so we have secondary sort column later
      //Always save 2 of them so we make sure we have a different one next time incase user clicks on the same column twice
      if ( $this->lastsort_cols[0] === FALSE || $this->lastsort_cols[0] != $this->default_col ) {
      	$this->lastsort_cols[1] = $this->lastsort_cols[0];
      	$this->lastsort_cols[0] = $this->default_col;
      }
      $this->last_sort_action = $this->sort_direction;
      $this->last_sort_column = $this->default_col;
	}

	//Prepares the pagedata[] for reversed output
	private function prepare_page_reverse() {
		$this->isreversed = TRUE;
	   $this->datapage = array();
	   $this->save_last_info();
	   if ( $this->total <= 0 ) {
	   	return FALSE;
	   }
	   $start_index = $this->total - (($this->current_page-1) * $this->perpage) - 1;
	   //$end_index = $start_index + $this->perpage - 1;
	   $end_index = $start_index - $this->perpage + 1;
	   if ( $start_index >= $this->total ) {
	   	$start_index = $this->total - 1;
	   }
	   if ( $end_index < 0 ) {
	   	$end_index = 0;
	   }
		for ( $i = $start_index; $i >= $end_index; $i-- ) {
			$this->datapage[] = & $this->data[$i];
		}
	   if ( $this->allpage ) {
	   	$this->counter = $this->total - 1;
	   }
		return TRUE;
	}

	//Adds one data row
	function add(& $data) {
		$this->data[] = $data;
	}

	//Reset the counter for next() and some other init. stuff
	function reset() {
		$this->init_compress();
		$this->total = count($this->data);
		$this->counter = 0;
		if ( $this->isreversed && $this->allpage ) {
			$this->counter = $this->total - 1;
		}
		//$this->title_nobr = TRUE;
		$this->totalpage = count($this->datapage);
		//$this->pagetitle = rawurldecode($this->pagetitle);
		if ( $this->sort_refresh_divid_unqiue ) {
			//(issue: 187) - Can't sort if more than 2 open sort/displaylist tables.
			//$this->sort_refresh_divid = 'displaylist_box_' . $this->sort_refresh_divid_unqiue_counter++;
			$this->sort_refresh_divid = 'displaylist_box_' . time();
		}
	}

	//Destroys all the user added data
	function reset_data() {
		$this->data = array();
		$this->data_compressed = '';
		$this->datapage = array();
		$this->isreversed = FALSE;
		$this->reset();
	}

	//Get the next data row. Always call reset() before using the function for the first time.
	function next() {
		if ( $this->allpage === TRUE ) {
			if ( !$this->isreversed ) {
				if ( $this->total <= $this->counter ) {
					return FALSE;
				}
				return $this->prepare_data($this->data[$this->counter++]);
			}
			else {
				if ( $this->counter < 0 ) {
					return FALSE;
				}
				return $this->prepare_data($this->data[$this->counter--]);
			}
		}
		else {
			if ( $this->totalpage <= $this->counter ) {
				return FALSE;
			}
			return $this->prepare_data($this->datapage[$this->counter++]);
		}
	}

	//Prepare a single array element. This mainly strips out all the hidden data
	//since the code outside doesn't understand them and we don't want to present them in any way.
	//It will also merge the hidden template strings if any with the actual data.
	private function prepare_data(&$data) {
		global $g_pdf_mode;
		if ( !is_array($data) ) {
			return FALSE;
		}
		$new_data = array();
		foreach ($data as $key=>$value) {
			if ( !is_numeric($key) ) {
				continue;
			}
			if ( array_key_exists(self::HIDEDATA_PREFIX . $key, $data) && array_key_exists($key, $this->data_template_strings) &&
				  (!isset($g_pdf_mode) || $g_pdf_mode !== TRUE ) ) {
				//Don't print the link if PDF
				$str = '';
				$evalstr = '$str=sprintf($this->data_template_strings[$key],' . $data[self::HIDEDATA_PREFIX . $key] . ');';
				@eval($evalstr);
				//The eval fails then it will just use the plain data so the user still sees the data
				//but just no links (most template strings are javascript links).
				if ( !empty($str) ) {
					$str = str_replace(self::HIDEDATA_REPLACE_STR, $value, $str);
					$new_data[$key] = $str;
					continue;
				}
			}
			$new_data[$key] = $value;
		}
		return $new_data;
	}

	private function init_compress() {
      if ( $this->CI->config->item('cms_searchsession_compress') === TRUE ) {
         if ( property_exists($this, 'data_compressed') && !empty($this->data_compressed) && count($this->data) <= 0 ) {
            $this->data = & osa_decompress($this->data_compressed);
            if ( $this->data == FALSE ) {
            	//This means no compression method available and data was stored as a serialized string
            	$this->data = @unserialize($this->data_compressed);
            	if ( !is_array($this->data) ) {
            		//Unexpected behavior, should never happen. If it does then user will get blank page.
            		$this->data = array();
            		osa_errorlog(__METHOD__, ' - decompress failed for unknown reason.');
            	}
            }
         }
      }
	}

	//Get the number of pages
	public function page_total() {
	  //Figure out how many pages
	   $this->total = count($this->data);
      $tmp_total_page = $this->total / $this->perpage;
      if ( $tmp_total_page < 1 ) {
         $tmp_total_page = 1;
      }
      elseif ($tmp_total_page > (int) $tmp_total_page) {
         $tmp_total_page = (int) $tmp_total_page + 1;
      }
      return (int) $tmp_total_page;
	}

	//Loads the view in $this->view_file. This is for full view refresh
	public function load_view() {
		if ( !empty($this->view_file) ) {
		    $this->CI->load->view($this->view_file, array('display'=>$this));
		    return TRUE;
		}
		return FALSE;
	}

   public function load_tableview() {
      $this->CI->load->view('mapping/display_list_view');
   }

   //Prints the page link only when $this->allpage is false
	public function print_page_links() {
		if ( $this->allpage ) {
			return;
		}
      $total_page = $this->page_total();
      $showpages = $this->CI->config->item('cms_max_showpage');
      if ( $showpages < self::MIN_SHOW_PAGE_NUMBERS ) {
      	$showpages = self::MIN_SHOW_PAGE_NUMBERS;
      }
      elseif ($showpages > self::MAX_SHOW_PAGE_NUMBERS ) {
      	$showpages = self::MAX_SHOW_PAGE_NUMBERS;
      }
      $url = base_url() . "search/table/fullview/{$this->divid}/";
      $update_divid = $this->divid;
      if ( !empty($this->refresh) ) {
      	//override divid
      	$update_divid = $this->refresh;
      }
      $link = "<a class=\"paginglink\" href=\"$url\" onClick=\"ajaxGetpage('$update_divid','%s',false);return false;\">%s</a>";
      if ( $this->iswindow ) {
      	//Dhtmlxwindow uses different Javascript function
      	$pagetitle = rawurldecode($this->pagetitle);
      	$link = "<a class=\"paginglink\" href=\"$url\" onClick=\"updateWindow('%s','','$pagetitle','$update_divid');return false;\">%s</a>";
      }
      $prev = $this->current_page - 1;
      $next = $this->current_page + 1;
      if ( $next > $total_page ) {
      	$next = 0;
      }
      echo "<center><div style='margin:0px;padding:0px;font-size:15px'><nobr>" . lang('map_list_page') . ' ';
      if ( $prev > 0 ) {
         $tmp_link = sprintf($link, $url.$prev, lang('map_list_prev'));
         echo " $tmp_link ";
      }
      elseif ( $total_page>1 ) {
      	echo lang('map_list_prev') . ' ';
      }
	   if ( $this->current_page-$showpages > 1 ) {
         $tmp_link = sprintf($link, $url."1", "1");
         echo $tmp_link;
	    	if ( $this->current_page-$showpages > 2 ) {
          	echo ' ... ';
         }
      }
      for ($i=$this->current_page-$showpages; $i<$this->current_page; $i++) {
      	if ( $i>0 ) {
      		$tmp_link = sprintf($link, $url.$i, "$i");
            echo " $tmp_link ";
      	}
      }
      echo "<b><font size=\"+1\">$this->current_page</font></b>";
      $tmp_end_page = $this->current_page+$showpages;
	   for ($i=$this->current_page+1; $i<=$tmp_end_page; $i++) {
	   	if ( $i <= $total_page ) {
            $tmp_link = sprintf($link, $url.$i, "$i");
            echo " $tmp_link ";
	   	}
      }
      if ( $i <= $total_page) {
     	    $tmp_link = sprintf($link, $url.$total_page, "$total_page");
     	    if ( $i < $total_page ) {
     	    	echo ' ... ';
     	    }
          echo $tmp_link;
      }
	  if ( $next > 0 ) {
         $tmp_link = sprintf($link, $url.$next, lang('map_list_next'));
         echo " $tmp_link ";
      }
		elseif ( $total_page>1 ) {
      	echo ' ' . lang('map_list_next');
      }
      echo "</nobr></div></center>";
	}

	//Don't save the session
	function no_session() {
		$this->session_saved = TRUE;
	}

	//It was a nightmare to have this function as the destrctor or sleep (if there are multiple copies)
	//and manual session update is much safer. We couldn't make this into a singleton either...
   private function save_session() {
   	if ( $this->session_saved ) {
   		return;
   	}
   	@osa_php_session_start();
   	//Clean unneeded page data since the next page will be different
   	$this->datapage = array();
      if ( $this->use_compression ) {
         if ( property_exists($this, 'data_compressed') && !empty($this->data_compressed) ) {
         	//If we already have a compressed version then just destroy the non-compressed version
         	//Code in this class can set data_compressed to empty string to force compress update
            $this->data = array();
          }
          else {
          	//We have no compression version so check...
             $tmp_str = & serialize($this->data);
             if ( strlen($tmp_str) > $this->compression_threshold ) {
             	 //Do clean up and compression
                $this->data = array();
                //PHP session always rewrites everything so no performance difference to
                //separate this or put it in the object.
                $this->data_compressed = & osa_compress($tmp_str, TRUE);
                if ( !$this->data_compressed ) {
                	//No compression method available so just save as a serialzed string
                	$this->data_compressed = & $tmp_str;
                }
                else {
                	//Data is compresses and $tmp_str is not needed and need to free up memory.
                	$tmp_str = '';
                }
             }
          }
      }
      //Without this it will write the whole CI object to session, yikes...
      $this->CI = NULL;
      //If we assign the object then it will cause PHP to always have 2 objects, big nono for memory management
      //since this thing can be big in some cases.
      //We rather want to always serialize the object and hope PHP is smart enough not to really serialize this again
      //during session file update...
      $_SESSION[self::SESSION_POSTFIX . $this->divid] = & serialize($this);
      //The current interface does not to save twice but just to be sure we don't save twice.
      $this->session_saved = TRUE;
	}

	public function add_urllink_template_str($column_number, $url) {
		if ( !osa_is_int($column_number) || $column_number < 0 || empty($url) ) {
			return FALSE;
		}
		$this->data_template_strings[$column_number] = $url;
		return TRUE;
	}

	public static function add_hidden_data($column_number, &$data, $sprintf_values) {
		//No checking since it is called many times... Always assume input params are correct
		//if ( !isset($data[self::HIDEDATA_PREFIX . $column_number]) ) {
		$data[self::HIDEDATA_PREFIX . $column_number] = $sprintf_values;
		//}
	}

	//Now we can use destruct since we made the object get function to always return the same static reference object
	function __destruct() {
		$this->save_session();
	}
}
?>