<?php

  # load modules
  $noSessionStart = true; // don't call session start (not needed for viewers)
  $libDir         = pathinfo(__FILE__, PATHINFO_DIRNAME);
  require_once "$libDir/init.php";

  // error checking
  if (!$SETTINGS['isInstalled']) { die("Error: You must install the program before you can use the viewers."); }

  // globals
  global $VIEWER_NAME;
  global $FORM;
  $FORM = $_REQUEST; // eventually we'll migrate from FORM to _REQUEST


/*
  list($records, $details) = getRecords(array(
    'tableName'     => 'listings', // REQUIRED, error if not specified, tableName is prefixed with $TABLE_PREFIX
    'where'         => '',         // optional, defaults to blank
    'orderBy'       => '',         // optional, defaults to table sort order if undefined
    'limit'         => '',         // optional, defaults to blank
    'offset'        => '',         // optional, defaults to blank (if set but no limit then limit is set to high number as per mysql docs)
    'perPage'       => '',         // optional, number of records to show per page - loads page number from $_REQUEST['page']
    'loadUploads'   => '',         // optional, defaults to yes, loads upload array into upload field
    'allowSearch'   => '',         // optional, defaults to yes, adds search info from query string
    'requireSearchMatch' => '',         // optional, don't show any results unless search keyword submitted and matched
    'loadCreatedBy' => '',         // optional, defaults to yes, adds createdBy. fields for created user
    'loadListDetails' => '',       // optional, defaults to yes, adds $details with prev/next page, etc info
    'orWhere'       => '',         // optional, adding " OR ... " to end of where clause
    'useSeoUrls'    => false,      // optional, use SEO urls, defaults to no
    'debugSql'      => false,      // optional, display SQL query, defaults to no
  ));
*/
function getRecords($options) {
  global $VIEWER_NAME, $TABLE_PREFIX;
  $VIEWER_NAME = "getRecords(" . @$options['tableName'] . ")";

  // error checking
  _getRecords_errorChecking($options);

  // load schema
  $schema = loadSchema($options['tableName']);
  if (!$schema) { die("$VIEWER_NAME: Couldn't load schema for '" .htmlspecialchars($options['tableName']). "'!"); }


  // set defaults
  if (!array_key_exists('orderBy', $options))         { $options['orderBy'] = $schema['listPageOrder']; } // default orderBy to section editor OrderBy value
  if (!array_key_exists('loadUploads', $options))     { $options['loadUploads']     = true; }
  if (!array_key_exists('allowSearch', $options))     { $options['allowSearch']     = true; }
  if (!array_key_exists('requireSearchMatch', $options)) { $options['requireSearchMatch']   = false; }
  if (!array_key_exists('loadCreatedBy', $options))   { $options['loadCreatedBy']   = true; }
  if (!array_key_exists('loadListDetails', $options)) { $options['loadListDetails'] = true; }

  $options['pageNum'] = @$options['pageNum'] ? @$options['pageNum'] : max(@$_REQUEST['page'], 1);
  $options['limit']   = @$options['perPage'] ? $options['perPage'] : @$options['limit'];
  $options['offset']  = @$options['perPage'] ? (($options['pageNum']-1) * $options['perPage']) + @$options['offset'] : @$options['offset'];
  if ($options['offset'] && !$options['limit']) { $options['limit'] = 1000000; } // if offset and no limit set limit to high number as per MySQL docs


  // Get records
  list($rows, $totalRecords) = _getRecords_loadResults($options, $schema);

  // Add uploads
  if (@$options['loadUploads'])   { _getRecords_addUploadFields($rows, $options, $schema); }

  // Add createdBy.fields to records
  if (@$options['loadCreatedBy'] && @$schema['createdByUserNum']) { _getRecords_joinTable($rows, $options, 'accounts'); }

  // Add joinTable fields
  if (@$options['joinTable']) { _getRecords_joinTable($rows, $options); }

  // get List Details
  $listDetails = array();
  if ($options['loadListDetails']) {
    $listDetails = _getRecords_getListDetails($options, count($rows), $totalRecords, $schema);
  }

  //
  return array($rows, $listDetails, $schema);
}


//
function _getRecords_errorChecking($options) {
  global $VIEWER_NAME;

  ### error checking
  $errors = "";
  if     (!is_array($options))     { $errors .= "First argument for getRecords() must be an array!<br/>\n"; }
  elseif (!@$options['tableName']) { $errors .= "No 'tableName' value specified in options!<br/>\n"; }

  // check for unknown options!
  $validOptions = array('tableName', 'where', 'orWhere', 'orderBy', 'limit', 'offset', 'perPage', 'loadUploads', 'allowSearch', 'requireSearchMatch', 'loadCreatedBy', 'useSeoUrls', 'loadListDetails', 'joinTable', 'debugSql');
  foreach (array_keys($options) as $optionName) {
    if (!in_array($optionName, $validOptions)) {
      $validOptionsAsCSV = implode(', ', $validOptions);
      $errors .= "Unknown option '$optionName' specified.  Valid option names are: ($validOptionsAsCSV)<br/>\n";
    }
  }


  //
  if (@$options['perPage'] && (@$options['limit'] || @$options['offset'])) {
    $errors .= "Can't set both 'perPage' and 'limit' or 'offset' options at the same time, choose one!<br/>\n";
  }


  if ($errors) { die("$VIEWER_NAME errors<br/>\n$errors"); }

}

//
function _getRecords_loadResults($options, $schema) {
  global $VIEWER_NAME, $TABLE_PREFIX;

  // create where
  $where = @$options['where'];
  if ($options['allowSearch']) {
    $defaultWhere = _createDefaultWhereWithFormInput($schema, '');
    if ($options['requireSearchMatch'] && !$defaultWhere) { $defaultWhere = "0 = 1"; } // always false

    if     (!$where)                 { $where = $defaultWhere; }
    elseif ($where && $defaultWhere) { $where = "$where AND $defaultWhere"; }
  }
  if (@$schema['createdByUserNum'] && @$schema['_hideRecordsFromDisabledAccounts']) {
    if ($where) { $where .= " AND "; }
    $subquery = "SELECT num FROM `{$TABLE_PREFIX}accounts` WHERE disabled != 1 AND (expiresDate > NOW() OR neverExpires = 1)";
    $where   .= "createdByUserNum IN ($subquery)";
  }
  $where = _addWhereConditionsForSpecialFields($schema, $where); // adds WHERE to beginning of string, do this LAST
  if (@$options['orWhere']) {
    $where = preg_replace("/^\s*WHERE\s*/i", "", $where); // remove WHERE keyword
    if ($where) { $where = "($where) OR {$options['orWhere']}"; }
    else        { $where = $options['orWhere']; }
    if ($where) { $where = " WHERE $where"; }
  }

  // create query
  $query            = "SELECT SQL_CALC_FOUND_ROWS * FROM `$TABLE_PREFIX{$options['tableName']}`\n";
  $query           .= "$where\n";                                                       // WHERE
  $query           .= (@$options['orderBy']) ? " ORDER BY {$options['orderBy']}" : ""; // ORDER BY
  if (@$options['limit'])  { $query .= "\n LIMIT "  . (int) $options['limit']; }
  if (@$options['offset']) { $query .= "\nOFFSET " . (int) $options['offset']; }
  if (@$options['debugSql']) { print "<xmp>$query</xmp>"; }

  // execute query
  $result = mysql_query($query) or die("$VIEWER_NAME MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");
  $records   = array();
  while ($record = mysql_fetch_assoc($result)) {
    $filenameValue       = getFilenameFieldValue($record, @$schema['_filenameFields']);
    $record['_filename'] = rtrim($filenameValue, '-');

    if    (@!$schema['_detailPage']) { $record['_link'] = "javascript:alert('Set Detail Page Url for this section in: Admin > Section Editors > Viewer Urls')"; }
    elseif(@$options['useSeoUrls'])  { $record['_link'] = @$schema['_detailPage'] . '/' . $filenameValue . $record['num'] . "/"; }
    else                             { $record['_link'] = @$schema['_detailPage'] . '?' . $filenameValue . $record['num']; }

    array_push($records, $record);
  }
  if (is_resource($result)) { mysql_free_result($result); }



   // modify field values
  foreach ($schema as $fieldname => $fieldSchema) {
    if (!is_array($fieldSchema)) { continue; } // fields are stored as arrays, other entries are table metadata

    // assign checkbox values
    if (@$fieldSchema['type'] != 'checkbox') {
      if (array_key_exists('checkedValue', $fieldSchema)) { // skip checkbox fields without checked/unchecked values
        foreach (array_keys($records) as $index) {
          $record = &$records[$index];
          $record[$fieldname] = $record[$fieldname] ? $fieldSchema['checkedValue'] : $fieldSchema['uncheckedValue'];
        }
      }
    }

  }



  // get record count
  $query        = "SELECT FOUND_ROWS()";
  if (@$options['debugSql']) { print "<xmp>$query</xmp>"; }
  $countResults = mysql_query($query) or die("$VIEWER_NAME: MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");
  $countRow     = mysql_fetch_row($countResults);
  $totalRecords = (int) $countRow[0];
  if (is_resource($countResults)) { mysql_free_result($countResults); }

  return array($records, $totalRecords);
}


//
function _getRecords_joinTable(&$rows, $options, $joinTable = '') {
  global $VIEWER_NAME, $TABLE_PREFIX;

  $joinTable  = $joinTable ? $joinTable : $options['joinTable'];
  $isAccounts = ($joinTable == 'accounts');
  $joinFieldA = 'createdByUserNum';
  $joinFieldB = $isAccounts ? 'num' : 'createdByUserNum';


  // get fieldA values as CSV
  $fieldAValues = array();
  foreach ($rows as $row) {
    if ($row[$joinFieldA]) { $fieldAValues[] = $row[$joinFieldA]; }
  }
  $fieldAValues = array_unique($fieldAValues);
  if (!$fieldAValues) { return; }
  $fieldAValuesAsCSV = implode(',', $fieldAValues);

  // load rows
  list($joinrows,,$schema) = getRecords(array(
    'tableName'       => $joinTable,
    'where'           => "`$joinFieldB` IN ($fieldAValuesAsCSV)",
    'loadUploads'     => $options['loadUploads'],
    'allowSearch'     => false,
    'loadCreatedBy'   => false,
    'loadListDetails' => false,
    'useSeoUrls'      => @$options['useSeoUrls'],
    'debugSql'        => @$options['debugSql'],
  ));

  // get join rows by num
  $joinRowsByNum = array();
  foreach ($joinrows as $record) {
    $joinRowsByNum[ $record[$joinFieldB] ] = $record;
  }

  // get tableBfields
  $joinTableFields = array();
  foreach ($schema as $fieldname => $fieldSchema) {
    if (!is_array($fieldSchema)) { continue; }
    if (@$fieldSchema['type'] == 'separator') { continue; }
    $joinTableFields[] = $fieldname;
  }
  $joinTableFields[] = "_filename";
  $joinTableFields[] = "_link";

  // add tableB rows
  $fieldnamePrefix = $isAccounts ? 'createdBy' : $joinTable;
  foreach (array_keys($rows) as $index) {
    $record     = &$rows[$index];
    $joinRecord = @$joinRowsByNum[$record[$joinFieldA]];

    foreach ($joinTableFields as $fieldname) {
      if ($isAccounts && $fieldname == 'password') { continue; }
      $record["$fieldnamePrefix.$fieldname"] = @$joinRecord[$fieldname];
    }
  }
}

//
function _getRecords_addUploadFields(&$rows, $options, $schema) {
  global $VIEWER_NAME, $TABLE_PREFIX;
  if (@!$options['loadUploads']) { return; }

  // get recordNums
  $recordNums = array();
  foreach ($rows as $record) {
    if (@$record['num']) { $recordNums[] = $record['num']; }
  }
  if (!$recordNums) { return; }

  // get upload fields
  $uploadFields = array();
  $uploadFieldsAsCSV = "";
  foreach ($schema as $fieldname => $fieldSchema) {
    if (!is_array($fieldSchema))           { continue; }  // fields are stored as arrays, other entries are table metadata, skip metadata
    if (@$fieldSchema['type'] != 'upload') { continue; }  // skip all but upload fields
    if ($uploadFieldsAsCSV) { $uploadFieldsAsCSV .= ","; }
    $uploadFields[]     = $fieldname;
    $uploadFieldsAsCSV .= "'$fieldname'";
  }

  // load uploads
  $uploadsByNumAndField = array();
  $recordNumsAsCSV   = implode(',', $recordNums);
  if ($recordNumsAsCSV && $uploadFieldsAsCSV) {
    $query   = "SELECT * FROM `{$TABLE_PREFIX}uploads`\n";
    $query  .= " WHERE tableName = '" .mysql_real_escape_string($options['tableName']). "' AND\n";
    $query  .= "       fieldName IN ($uploadFieldsAsCSV) AND\n";
    $query  .= "       recordNum IN ($recordNumsAsCSV)\n";
    $query  .= " ORDER BY `order`, num";
    if (@$options['debugSql']) { print "<xmp>$query</xmp>"; }

    $result  = mysql_query($query) or die("MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");
    while ($record = mysql_fetch_assoc($result)) {
      $record['filename']     = pathinfo($record['filePath'], PATHINFO_BASENAME);
      $record['extension']    = pathinfo($record['filePath'], PATHINFO_EXTENSION);
      $record['isImage']      = preg_match("/\.(gif|jpg|jpeg|png)$/i", $record['filePath']);
      $record['hasThumbnail'] = $record['isImage'] && $record['thumbUrlPath'];
      $uploadsByNumAndField[$record['recordNum']][$record['fieldName']][] = $record;
    }
    if (is_resource($result)) { mysql_free_result($result); }
  }

  // add uploads to records
  foreach (array_keys($rows) as $index) {
    $record = &$rows[$index];

    foreach ($uploadFields as $fieldname) {
      $record[$fieldname] = array();
      $uploadsArray    = @$uploadsByNumAndField[$record['num']][$fieldname];
      if ($uploadsArray) { $record[$fieldname] = $uploadsArray; }
    }
  }
}


//
function _getRecords_getListDetails($options, $rowCount, $totalRecords, $schema) {
  global $VIEWER_NAME;
  $details = array();

  ### get list details
  $details = array();
  $details['invalidPageNum']   = !$rowCount && $options['pageNum'] > 1;
  $details['noRecordsFound']   = !$rowCount && $options['pageNum'] == 1;
  $details['page']             = $options['pageNum'];
  $details['perPage']          = @$options['perPage'];

  $details['totalPages']       = 1;
  if (@$options['perPage'] && $totalRecords > $options['perPage']) {
    $details['totalPages'] = ceil($totalRecords / $options['perPage']);
  }

  $details['totalRecords']     = $totalRecords;
  $details['pageResultsStart'] = min($totalRecords, $options['offset'] + 1);
  $details['pageResultsEnd']   = min($totalRecords, $options['offset'] + $options['limit']);

  # get page nums
  $_minOfPageNumAndTotalPages    = min($options['pageNum'], $details['totalPages']);
  $details['prevPage']       = ($_minOfPageNumAndTotalPages > 1) ? $_minOfPageNumAndTotalPages-1 : '';
  $details['nextPage']       = ($_minOfPageNumAndTotalPages < $details['totalPages']) ? $_minOfPageNumAndTotalPages+1 : '';
  if ($details['invalidPageNum']) {
    $details['prevPage'] = $details['totalPages'];
  }

  # pass query arguments forward in page links
  $extraQueryArgs    = "";
  $extraPathInfoArgs = "";
  foreach ($_REQUEST as $key => $value) {
    if ($key == 'page')  { continue; } // skip page value, we set it below
    $extraQueryArgs    .= urlencode($key) .'='. urlencode($value) . '&';
    $extraPathInfoArgs .= urlencode($key) .'-'. urlencode($value) . '/';
  }

  # get page links
  $listViewer = $_SERVER['SCRIPT_NAME'];
  if (@$options['useSeoUrls']) {
    $details['prevPageLink']   = "$listViewer/{$extraPathInfoArgs}page-{$details['prevPage']}/";
    $details['nextPageLink']   = "$listViewer/{$extraPathInfoArgs}page-{$details['nextPage']}/";
    $details['firstPageLink']  = "$listViewer/{$extraPathInfoArgs}page-1/";
    $details['lastPageLink']   = "$listViewer/{$extraPathInfoArgs}page-{$details['totalPages']}/";
  }
  else {
    $details['prevPageLink']  = "$listViewer?{$extraQueryArgs}page={$details['prevPage']}";
    $details['nextPageLink']  = "$listViewer?{$extraQueryArgs}page={$details['nextPage']}";
    $details['firstPageLink'] = "$listViewer?{$extraQueryArgs}page=1";
    $details['lastPageLink']  = "$listViewer?{$extraQueryArgs}page=" . $details['totalPages'];
  }

  //
  $details['_detailPage'] = @$schema['_detailPage'];
  $details['_listPage']   = @$schema['_listPage'] ? $schema['_listPage'] : "javascript:alert('Set List Page Url for this section in: Admin > Section Editors > Viewer Urls')"; ;

  return $details;
}



//
function whereRecordNumberInUrl($altWhere = null) {
  $num   = getNumberFromEndOfUrl();

  //
  $where = "";
  if (!$num && $altWhere) { $where = $altWhere; }
  else                    { $where = "num = '$num'"; }

  //
  return $where;

}


/*
  list($categoryRecords, $selectedCategory) = getCategories(array(
    'tableName'           => 'category',  // REQUIRED
    'selectedCategoryNum' => '',          // defaults to getNumberFromEndOfUrl()
    'categoryFormat'      => '',          // default to 'showall'.  Options: showall, onelevel, twolevel
  ));
*/
function getCategories($options) {
  $VIEWER_NAME = "Category Viewer ({$options['tableName']})";

  // error checking
  $errors       = "";
  $validOptions = array('tableName', 'useSeoUrls', 'debugSql',
                        'selectedCategoryNum','categoryFormat');
  $validFormats = array('','showall','onelevel','twolevel');
  if     (!is_array($options))     { $errors .= "First argument for getRecords() must be an array!<br/>\n"; }
  elseif (!@$options['tableName']) { $errors .= "No 'tableName' value specified in options!<br/>\n"; }
  else { // check options are value

    $unknownOptions  = array_diff(array_keys($options), $validOptions);
    foreach ($unknownOptions as $optionName) { $errors .= "Unknown option '$optionName' specified<br/>\n"; }
    if ($unknownOptions)                     { $errors .= "Valid option names are: (" .join(', ', $validOptions). ")<br/>\n"; }
  }
  if (!in_array(@$options['categoryFormat'], $validFormats)) {
    $errors .= "categoryFormat must be one of: " .join(", ", $validFormats). "<br/>\n";
  }
  if ($errors) { die("$VIEWER_NAME errors<br/>\n$errors"); }

  // set defaults
  if (!@$options['selectedCategoryNum']) { $options['selectedCategoryNum'] = getNumberFromEndOfUrl(); }
  if (!@$options['categoryFormat'])      { $options['categoryFormat']      = 'showall'; }


  // load all categories
  list($categoryRecords) = getRecords(array(
    'tableName'       => $options['tableName'],
    'useSeoUrls'      => @$options['useSeoUrls'], // optional, use SEO urls, defaults to no
    'debugSql'        => @$options['debugSql'],   // optional, display SQL query, defaults to no
    'loadUploads'     => false,    // optional, defaults to yes, loads upload array into upload field
    'allowSearch'     => false,    // optional, defaults to yes, adds search info from query string
    'loadCreatedBy'   => false,    // optional, defaults to yes, adds createdBy. fields for created user
    'loadListDetails' => false,    // optional, defaults to yes, adds $details with prev/next page, etc info
  ));

  // get category format rules
  if      ($options['categoryFormat'] == 'showall')  { $rootDepthVisible = 'all'; $childDepthVisible = 'all'; $parentVisibility = 'parentBranches'; }
  else if ($options['categoryFormat'] == 'onelevel') { $rootDepthVisible = '1';   $childDepthVisible = '1';   $parentVisibility = 'parentBranches'; }
  else if ($options['categoryFormat'] == 'twolevel') { $rootDepthVisible = '2';   $childDepthVisible = '1';   $parentVisibility = 'parentBranches'; }
  else { die("Unknown category format '" .htmlspecialchars($options['categoryFormat'])."'!"); }

  // get selectedCategory
  $selectedCategory = array();
  foreach ($categoryRecords as $category) {
    if ($category['num'] == $options['selectedCategoryNum']) {
      $selectedCategory = $category;
      break;
    }
  }

  // get displayed categories
  $displayedCategories = array();
  for ($index = 0; $index < count($categoryRecords); $index++) {
    $category = $categoryRecords[$index];

    // show category?
    $showThisCategory = _categoryMatchesFormatRules($category, $selectedCategory, $rootDepthVisible, $childDepthVisible, $parentVisibility);
    if (!$showThisCategory) { continue; } # skip categories not matching categoryFormat rules

    // add pseudo fields
    $prevCategory       = ($index > 0)                         ? $categoryRecords[$index-1] : array();
    $nextCategory       = ($index < count($categoryRecords)-1) ? $categoryRecords[$index+1] : array();
    $category['_isSelected']    = (int) ($category['num'] == $options['selectedCategoryNum']);

    $category['_listItemStart'] = "";
    $category['_listItemEnd']   = "";
    if ($options['categoryFormat'] == 'showall') {
      $category['_listItemStart'] = _getListItemStartTags($prevCategory, $category, $nextCategory);
      $category['_listItemEnd']   = _getListItemEndTags($prevCategory, $category, $nextCategory);
    }

    //
    $displayedCategories[] = $category;
  }


  //
  return array($displayedCategories, $selectedCategory);

}


//
function _categoryMatchesFormatRules($category, $selectedCategory, $rootDepthVisible, $childDepthVisible, $parentVisibility) {

  # $category         - category record we're testing
  # $selectedCategory  - selected category record (if any)
  # $rootDepthVisible  - always show X level(s) of subcategories from root
  # $childDepthVisible - show Y level(s) of subcategories below selected category
  # $parentVisibility  - show Z above selected categories (nothing, parentsOnly, parentBranches)

  $showThisCategory = false;

  // depth always visible from root
  if      ($rootDepthVisible == 'all')             { $showThisCategory = true; }
  else if ($category['depth'] < $rootDepthVisible) { $showThisCategory = true; }

  // depth visible under child
  if ($selectedCategory) {
    $isChildOfSelected = preg_match("/:{$selectedCategory['num']}:/", $category['lineage']);
    if ($isChildOfSelected) {
      if ($childDepthVisible == 'all') { $showThisCategory = true; }
      if ($category['depth'] <= ($selectedCategory['depth'] + $childDepthVisible)) { $showThisCategory = true; }
    }
  }

  // parentVisibility
  $directAncestorsOfSelected = preg_split('/:/', @$selectedCategory['lineage'],-1,PREG_SPLIT_NO_EMPTY);
  $isChildOfParentOrAncestor = in_array($category['parentNum'], $directAncestorsOfSelected);
  $isDirectParentOfSelected  = preg_match("/:{$category['num']}:/", @$selectedCategory['lineage']);
  if      ($parentVisibility == 'parentsOnly'    && $isDirectParentOfSelected)  { $showThisCategory = true; }
  else if ($parentVisibility == 'parentBranches' && $isChildOfParentOrAncestor) { $showThisCategory = true; }

  //
  return $showThisCategory;
}


//
function _getListItemStartTags($prevCategory, $thisCategory, $nextCategory) {
  $listItemStart = '';

  # start new lists
  if ($thisCategory['depth'] && $thisCategory['depth'] > @$prevCategory['depth']) {

    foreach (array_reverse(range(@$prevCategory['depth']+1, $thisCategory['depth'])) as $depth) {
      $paddingMultiple    = ($depth * 2);
      $openingListPadding = str_repeat("  ", $paddingMultiple);
      $listItemStart        .= "\n$openingListPadding<ul>";
    }
  }

  # start this item
  $paddingMultiple     = ($thisCategory['depth'] * 2)+1;
  $openingItemPadding  = str_repeat("  ", $paddingMultiple);
  $listItemStart      .= "\n$openingItemPadding<li>";

  #
  return $listItemStart;
}


//
function _getListItemEndTags($prevCategory, $thisCategory, $nextCategory) {
  $listItemEnd = '';

  # close this item
  if (@$nextCategory['depth'] <= $thisCategory['depth']) {
    $listItemEnd .= "</li>";
  }
  # close prev lists and items
  if ($thisCategory['depth'] && $thisCategory['depth'] > @$nextCategory['depth']) {
    $thisCategoryDepth = $thisCategory['depth'];
    $nextCategoryDepth = @$nextCategory['depth']+1;

    foreach (array_reverse(range($nextCategoryDepth, $thisCategoryDepth)) as $depth) {
      $paddingMultiple     = $depth * 2;
      $closingListPadding  = str_repeat("  ", ($paddingMultiple));
      $closingItemPadding  = @str_repeat("  ", ($paddingMultiple-1));
      $listItemEnd        .= "\n$closingListPadding</ul>";
      $listItemEnd        .= "\n$closingItemPadding</li>";
    }
  }

  #
  $isLastCategory = @$nextCategory['num'] ? 0 : 1;
  if ($isLastCategory) { $listItemEnd .= "\n"; }

  #
  return $listItemEnd;
}


//
function getListRows($options) {
  global $VIEWER_NAME, $TABLE_PREFIX;
  $VIEWER_NAME = "List Viewer ({$options['tableName']})";

  // error checking
  $requiredOptions = array('tableName');
  $validOptions    = array('tableName', 'titleField', 'perPage', 'where', 'orderBy', 'viewerUrl', 'pageNum', 'useSeoUrls');
  $errors          = _getOptionErrors($requiredOptions, $validOptions, $options);
  if ($errors) { die("$VIEWER_NAME errors<br/>\n$errors"); }

  // set defaults
  if (!@$options['pageNum'])   { $options['pageNum']   = @$_REQUEST['page']; }
  if (!@$options['pageNum'])   { $options['pageNum']   = "1"; } // default to page 1
  if (!@$options['perPage'])   { $options['perPage']   = 10; }
  if (!@$options['viewerUrl']) { $options['viewerUrl'] = "No_viewerUrl_value_specified_in_options"; }

  // get absolute url for viewer
  if (@$options['useSeoUrls'] && @$options['viewerUrl'] && !preg_match("|[/]|", $options['viewerUrl'])) {
    $options['viewerUrl'] = dirname($_SERVER['SCRIPT_NAME']) ."/". $options['viewerUrl'];
    $options['viewerUrl'] = preg_replace("|^[\\\\/]+|", "/", $options['viewerUrl']); // remove multiple leading slashes (and replace \ returned by dirname on windows in root)
  }

  # create query
  $schema           = loadSchema($options['tableName']);
  $fullTableName    = getTableNameWithPrefix($options['tableName']);
  $escapedTableName = mysql_real_escape_string($fullTableName);

  if (@$options['where'] != "") { $where = @$options['where']; }
  else                          { $where = _createDefaultWhereWithFormInput($schema, @$options['where']); }

  $where            = _addWhereConditionsForSpecialFields($schema, $where);
  $orderBy          = (@$options['orderBy']) ? "ORDER BY {$options['orderBy']}" : "";
  $offset           = ($options['pageNum']-1) * $options['perPage'];
  $limit            = "LIMIT " .mysql_real_escape_string($options['perPage']). " OFFSET " .mysql_real_escape_string($offset);
  $query            = "SELECT SQL_CALC_FOUND_ROWS * FROM `$escapedTableName` $where $orderBy $limit";

  # execute query
  $result     = mysql_query($query) or die("$VIEWER_NAME: MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");
  $rows       = array();
  while ($record = mysql_fetch_assoc($result)) {

    $filenameValue   = getFilenameFieldValue($record, @$options['titleField']);
    $record['_link'] = _getLink($options['viewerUrl'], $filenameValue, $record['num'], @$options['useSeoUrls']);

    array_push($rows, $record);
  }
  $listDetails = _getListDetails($options, count($rows));

  //
  return array($rows, $listDetails);
}

//
function _createDefaultWhereWithFormInput($schema, $where) {
  global $VIEWER_NAME;
  if ($where != "") { return $where; } // don't set default where if it's already been specified
  $seenQueries     = array();

  //
  $andConditions = array();
  foreach ($_REQUEST as $name => $value) {
    if ($value == '') { continue; } // skip fields with empty values
    if (!preg_match("/^(.+?)(_min|_max|_match|_keyword|_prefix|_query|)$/", $name, $matches)) { continue; } // skip fields without search suffixes
    $fieldnamesAsCSV = $matches[1];
    $searchType      = $matches[2];
    $fieldnames      = explode(',',$fieldnamesAsCSV);
    $orConditions    = array();


    foreach ($fieldnames as $fieldnameString) {

      // get field value for date searches
      if (preg_match("/^(.+?)_(year|month|day)$/", $fieldnameString, $matches)) {
        $fieldname = $matches[1];
        $dateValue = $matches[2];
        if (!is_array(@$schema[$fieldname])) { continue; } // skip invalid fieldnames
        if     ($dateValue == 'year')        { $fieldValue = "YEAR(`$fieldname`)"; }
        elseif ($dateValue == 'month')       { $fieldValue = "MONTH(`$fieldname`)"; }
        elseif ($dateValue == 'day')         { $fieldValue = "DAYOFMONTH(`$fieldname`)"; }
        else                                 { die("unknown date value '$dateValue'!"); }
      }

      // get field value for everything else
      else {
        if (!is_array(@$schema[$fieldnameString])) { continue; } // skip invalid fieldnames
        $fieldValue = "`$fieldnameString`";
      }

      // add conditions
      $fieldSchema = @$schema[$fieldnameString];
      $isMultiList = @$fieldSchema['type'] == 'list' && (@$fieldSchema['listType'] == 'pulldownMulti' || @$fieldSchema['listType'] == 'checkboxes');

      if (!$fieldValue) { die("No fieldValue defined!"); }
      if      ($searchType == '_min')     { $orConditions[] = "$fieldValue+0 >= '"   .mysql_real_escape_string($value). "'"; }
      else if ($searchType == '_max')     { $orConditions[] = "$fieldValue+0 <= '"   .mysql_real_escape_string($value). "'"; }
      else if ($searchType == '_match' || $searchType == '') {
        if ($isMultiList) { $orConditions[] = "$fieldValue LIKE '%\\t" .mysql_real_escape_string($value). "\\t%'"; }
        else              { $orConditions[] = "$fieldValue = '"    .mysql_real_escape_string($value). "'"; }
      }
      else if ($searchType == '_keyword') { $orConditions[] = "$fieldValue LIKE '%" .escapeMysqlWildcards(mysql_real_escape_string($value)). "%'"; }
      else if ($searchType == '_prefix')  { $orConditions[] = "$fieldValue LIKE '" .escapeMysqlWildcards(mysql_real_escape_string($value)). "%'"; }
      else if ($searchType == '_query')   {
        if (@$seenQueries["$fieldnamesAsCSV=$searchType"]++) { continue; } // only add each query once since we're add all fields at once
        $orConditions[] = _getWhereForSearchQuery($value, $fieldnames, $schema);
      }
      else { die($VIEWER_NAME . ": Unknown search type '$searchType'!"); }
    }

    $condition = join(' OR ', $orConditions);
    if ($condition) { $andConditions[] = "($condition)"; }

  }

  $where = join(" AND ", $andConditions);

  return $where;
}



//
function _getListDetails($options, $rowCount) {
  global $VIEWER_NAME;

  # get record count
  $countResults = mysql_query("SELECT FOUND_ROWS()") or die("$VIEWER_NAME: MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");
  $countRow     = mysql_fetch_row($countResults);
  $totalRecords = (int) $countRow[0];

  # set defaults
  if ($options['pageNum'] == 0) { $options['pageNum'] = 1; }

  ### get list details
  $listDetails = array();
  $listDetails['invalidPageNum'] = !$rowCount && $options['pageNum'] > 1;
  $listDetails['noRecordsFound'] = !$rowCount && $options['pageNum'] == 1;
  $listDetails['page']           = $options['pageNum'];
  $listDetails['perPage']        = $options['perPage'];
  $listDetails['totalPages']     = ($totalRecords >= 1) ? ceil($totalRecords / $options['perPage']) : 1;
  $listDetails['totalRecords']   = $totalRecords;
  $listDetails['pageResultsStart'] = min($totalRecords, (($listDetails['page']-1) * $listDetails['perPage']) + 1);
  $listDetails['pageResultsEnd']   = min($totalRecords, $listDetails['page'] * $options['perPage']);

  # get page nums
  $_minOfPageNumAndTotalPages    = min($options['pageNum'], $listDetails['totalPages']);
  $listDetails['prevPage']       = ($_minOfPageNumAndTotalPages > 1) ? $_minOfPageNumAndTotalPages-1 : '';
  $listDetails['nextPage']       = ($_minOfPageNumAndTotalPages < $listDetails['totalPages']) ? $_minOfPageNumAndTotalPages+1 : '';
  if ($listDetails['invalidPageNum']) {
    $listDetails['prevPage'] = $listDetails['totalPages'];
  }

  # pass query arguments forward in page links
  $extraQueryArgs    = "";
  $extraPathInfoArgs = "";
  foreach ($_REQUEST as $key => $value) {
    if ($key == 'page')  { continue; } // skip page value, we set it below
    $extraQueryArgs    .= urlencode($key) .'='. urlencode($value) . '&';
    $extraPathInfoArgs .= urlencode($key) .'-'. urlencode($value) . '/';
  }

  # get page links
  $listViewer = $_SERVER['SCRIPT_NAME'];
  if (@$options['useSeoUrls']) {
    $listDetails['prevPageLink']   = "$listViewer/{$extraPathInfoArgs}page-{$listDetails['prevPage']}/";
    $listDetails['nextPageLink']   = "$listViewer/{$extraPathInfoArgs}page-{$listDetails['nextPage']}/";
    $listDetails['firstPageLink']  = "$listViewer/{$extraPathInfoArgs}page-1/";
    $listDetails['lastPageLink']   = "$listViewer/{$extraPathInfoArgs}page-{$listDetails['totalPages']}/";
  }
  else {
    $listDetails['prevPageLink']  = "$listViewer?{$extraQueryArgs}page={$listDetails['prevPage']}";
    $listDetails['nextPageLink']  = "$listViewer?{$extraQueryArgs}page={$listDetails['nextPage']}";
    $listDetails['firstPageLink'] = "$listViewer?{$extraQueryArgs}page=1";
    $listDetails['lastPageLink']  = "$listViewer?{$extraQueryArgs}page=" . $listDetails['totalPages'];
  }

  return $listDetails;
}


//
function getRecord($options) {
  global $VIEWER_NAME, $TABLE_PREFIX;
  $VIEWER_NAME = "Page Viewer ({$options['tableName']})";

  // error checking
  $requiredOptions = array('tableName');
  $validOptions    = array('tableName', 'recordNum', 'where','titleField', 'orderBy');
  $errors          = _getOptionErrors($requiredOptions, $validOptions, $options);
  if ($errors) { die("$VIEWER_NAME errors<br/>\n$errors"); }

  // set defaults
  $schema = loadSchema($options['tableName']);
  if (!@$options['recordNum'])          { $options['recordNum'] = getNumberFromEndOfUrl(); }
  if (@$schema['menuType'] == 'single') { $options['recordNum'] = "1"; }  // always load record 1 for single menus

  // get where condition
  $whereConditions  = '';
  $escapedRecordNum = mysql_real_escape_string( (int) $options['recordNum'] );
  if     ($options['where'])     { $whereConditions = $options['where']; }
  elseif ($options['recordNum']) { $whereConditions = "num = '$escapedRecordNum'"; }

  // get record
  $fullTableName    = getTableNameWithPrefix($options['tableName']);
  $escapedTableName = mysql_real_escape_string($fullTableName);
  $where            = _addWhereConditionsForSpecialFields($schema, $whereConditions);
  $orderBy          = (@$options['orderBy']) ? "ORDER BY {$options['orderBy']}" : "";
  $query            = "SELECT * FROM `$escapedTableName` $where $orderBy LIMIT 0, 1";
  $result           = mysql_query($query) or die("$VIEWER_NAME: MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");
  $record           = mysql_fetch_assoc($result);

  // add _link field
  if ($record) {
    $filenameValue   = getFilenameFieldValue($record, @$options['titleField']);
    $record['_link'] = _getLink($_SERVER['SCRIPT_NAME'], $filenameValue, $record['num'], @$options['useSeoUrls']);
  }

  // define upload fields
  if ($record) {
    foreach ($schema as $fieldname => $fieldSchema) {
      if (!is_array($fieldSchema)) { continue; } // not a field definition, table metadata field
      if (@$fieldSchema['type'] != 'upload') { continue; }  // skip all but upload fields
      $record[$fieldname] = "Use getUploads() function to list uploads (See code generator).\n";
    }
  }

  //
  return $record;
}


//
function getUploads($tableName, $fieldName, $recordNum) {
  global $TABLE_PREFIX;
  $uploads = array();
  // error checking
  if (!$tableName) { die(__FUNCTION__ . ": no 'tableName' value specified!"); }
  if (!$fieldName) { die(__FUNCTION__ . ": no 'fieldName' value specified!"); }
  if (!$recordNum) { die(__FUNCTION__ . ": no 'recordNum' value specified!"); }

  // get record uploads
  $tableNameWithoutPrefix = getTableNameWithoutPrefix($tableName);
  $query   = "   SELECT * FROM {$TABLE_PREFIX}uploads ";
  $query  .= "    WHERE tableName = '" .mysql_real_escape_string($tableNameWithoutPrefix). "' AND ";
  $query  .= "          fieldName = '" .mysql_real_escape_string($fieldName). "' AND";
  $query  .= "          recordNum = '" .mysql_real_escape_string($recordNum). "'";
  $query  .= " ORDER BY `order`, num";
  $result  = mysql_query($query) or die("MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");

  //
  while ($upload = mysql_fetch_assoc($result)) {
    $upload['filename']     = pathinfo($upload['filePath'], PATHINFO_BASENAME);
    $upload['extension']    = pathinfo($upload['filePath'], PATHINFO_EXTENSION);
    $upload['isImage']      = preg_match("/\.(gif|jpg|jpeg|png)$/i", $upload['filePath']);
    $upload['hasThumbnail'] = $upload['isImage'] && $upload['thumbUrlPath'];

    array_push($uploads, $upload);
  }

  return $uploads;
}


// Note: This function is EXPERIMENTAL and may change in future
function searchMultipleTables($searchTables, $searchOptions) {
  global $VIEWER_NAME, $TABLE_PREFIX;
  $VIEWER_NAME = "Search Multiple Tables";

  # error checking
  if (!@$searchOptions['perPage'])  { die("$VIEWER_NAME : No perPage option specified!\n"); }

  ### create subqueries
  $subqueries = array();
  foreach ($searchTables as $tablename => $tableOptions) {
    foreach (array('viewerUrl', 'searchFields', 'titleField') as $optionName) {
      if (!@$tableOptions[$optionName]) { die("$VIEWER_NAME : No '$optionName' option specified for searchTable '" .htmlspecialchars($tablename). "'!\n"); }
    }

    // get search fields
    $searchFieldsCSV   = "";
    foreach ($tableOptions['searchFields'] as $fieldname) {
      if ($searchFieldsCSV) { $searchFieldsCSV .= ", "; }
      $searchFieldsCSV .= "`" . mysql_real_escape_string($fieldname) . "`";
    }

    // create query
    $fullEscapedTable = mysql_real_escape_string($TABLE_PREFIX . $tablename);
    $schema           = loadSchema($tablename);
    $where            = _addWhereConditionsForSpecialFields($schema, '');
    $subquery         = "SELECT '$tablename' as `tablename`, num, `{$tableOptions['titleField']}` as `_title`, ";
    if (@$tableOptions['summaryField']) { $subquery .= "`{$tableOptions['summaryField']}` as `_summary`, "; }
    else                                { $subquery .= "'' as `_summary`, "; }
    $subquery        .= "CONCAT_WS('\\t', $searchFieldsCSV) as _content FROM `$fullEscapedTable` $where\n";

    $subqueries[] = $subquery;
  }

  # create query
  $schema = array('_content' => array()); // allow this field to be searched in _getWhereForSearchQuery
  $where  = _getWhereForSearchQuery($searchOptions['keywords'], array('_content'), $schema);
  $query       = "SELECT SQL_CALC_FOUND_ROWS * FROM (\n  " .implode('  UNION ', $subqueries). ") as combinedTable\n";
  if ($where) { $query .= "WHERE $where"; }
  $query      .= getMysqlLimit($searchOptions['perPage'], @$_REQUEST['page']);

  #

  ## execute query
  $rows = array();
  if ($searchOptions['keywords'] && $where) {
    if (@$searchOptions['debugSql']) { print "<xmp>$query</xmp>"; }
    $result     = mysql_query($query) or die("$VIEWER_NAME: MySQL Error: ". htmlspecialchars(mysql_error()) . "\n");

    while ($record = mysql_fetch_assoc($result)) {
      $detailUrl       = $searchTables[$record['tablename']]['viewerUrl'];
      $filenameValue   = ''; // not working yet... getFilenameFieldValue($record, $searchTables[$record['tablename']]['filenameFields']);
      $useSeoUrls      = false;
      $link            = _getLink($detailUrl, $filenameValue, $record['num'], $useSeoUrls);

      $record['_title']   = $record['_title'];
      $record['_summary'] = strip_tags($record['_summary']);
      $record['_link']    = $link;

      array_push($rows, $record);
    }


  }

  $searchOptions['pageNum'] = @$_REQUEST['page'];
  $listDetails = _getListDetails($searchOptions, count($rows));

  //
  return array($rows, $listDetails);
}


// returns page number from url.  matches: view.php/anythinghere-####.html
function getNumberFromEndOfUrl() {
  $recordNum = 0;
  $urlDataFields = array(@$_SERVER["PATH_INFO"], @$_SERVER["QUERY_STRING"]);
  foreach ($urlDataFields as $urlData) {
    if (preg_match("/\D*(\d+)(\D+)?$/", $urlData, $matches)) {
      $recordNum = $matches[1];
      break;
    }
  }
  return $recordNum;
}


// return errors for missing required options and invalid option names
function _getOptionErrors($requiredOptions, $validOptions, $options) {
  $errors = "";

  // check for required fields
  foreach ($requiredOptions as $requiredOptionName) {
    if (!$options[$requiredOptionName]) { $errors .= "No '$requiredOptionName' value specified in options.<br/>\n"; }
  }

  // check for invalid options
  foreach ($options as $optionName => $optionValue) {
    if (!in_array($optionName, $validOptions)) {
      $validOptionsAsCSV = implode(', ', $validOptions);
      $errors .= "Unknown option '$optionName' specified.  Valid option names are: ($validOptionsAsCSV)<br/>\n";
    }
  }

  return $errors;
}


//
function _addWhereConditionsForSpecialFields($schema, $extraConditions) {
  $where            = "";
  $conditions       = array();

  if ($extraConditions)        { array_push($conditions, "($extraConditions)"); }
  if (@$schema['hidden'])      { array_push($conditions, "hidden = 0"); }
  if (@$schema['publishDate']) { array_push($conditions, "publishDate <= NOW()"); }
  if (@$schema['removeDate'])  {
    $thisCondition  = "removeDate >= NOW()";  // NULL end date or future end date
    if (@$schema['neverRemove']) { $thisCondition .= " OR neverRemove = 1"; } // never expires checked
    array_push($conditions, "($thisCondition)");
  }
  if ($conditions) {
    $where = " WHERE " . implode(" AND ", $conditions) . " ";
  }

  return $where;
}


// return MySQL WHERE clause for google style query: +word -word "multi word phrase"
function _getWhereForSearchQuery($query, $fieldnames, $schema) {

  // error checking
  if (!is_array($fieldnames)) { die(__FUNCTION__ . ": fieldnames must be an array!"); }

  // parse out "quoted strings"
  $searchTerms = array();
  $quotedStringRegexp = "/([+-]?)(['\"])(.*?)\\2/";
  preg_match_all($quotedStringRegexp, $query, $matches, PREG_SET_ORDER);
  foreach ($matches as $match) {
    list(,$plusOrMinus,,$phrase) = $match;
    $phrase = trim($phrase);
    $searchTerms[$phrase] = $plusOrMinus;
  }
  $query = preg_replace($quotedStringRegexp, "", $query); // remove quoted strings

  // parse out keywords
  $keywords = preg_split('/[\\s,;]+/', $query);
  foreach ($keywords as $keyword) {
    $plusOrMinus = "";
    if (preg_match("/^([+-])/", $keyword, $matches)) {
      $keyword = preg_replace("/^([+-])/", "", $keyword, 1);
      $plusOrMinus = $matches[1];
    }

    $searchTerms[$keyword] = $plusOrMinus;
  }

  // create query
  $where = "";
  $conditions = array();
  foreach ($searchTerms as $term => $plusOrMinus) {
    if ($term == '') { continue; }
    $likeOrNotLike  = ($plusOrMinus == '-') ? "NOT LIKE" : "LIKE";
    $andOrOr        = ($plusOrMinus == '-') ? " AND " : " OR ";
    $termConditions = array();

    foreach ($fieldnames as $fieldname) {
      if (!is_array(@$schema[$fieldname])) { continue; } // fields are stored as arrays, other entries are table metadata
      $fieldname = trim($fieldname);
      $escapedKeyword = escapeMysqlWildcards( mysql_real_escape_string($term) );
      $termConditions[] = "`$fieldname` $likeOrNotLike '%$escapedKeyword%'";
    }

    if ($termConditions) {
      $conditions[] = "(" . join($andOrOr, $termConditions) . ")\n";
    }

  }

  //
  $where = join(" AND ", $conditions);
  return $where;
}


//
function _getLink($detailUrl, $filenameFieldValue, $recordNum, $useSeoUrls) {
  global $VIEWER_NAME;

  // error checking
  //

  // define vars
  $link          = "";

  // add http://domain for viewer
  $includesProto = preg_match("!^\w+://!", $detailUrl);
  if (!$includesProto) {
    $isHTTPS = @$_SERVER["https"] == 'on' || @$_SERVER['SERVER_PORT'] == 443;
    $link   .= $isHTTPS ? 'http://' : 'http://';
    $link   .= $_SERVER['HTTP_HOST'];
  }


  // add url path (without http, domain, query or PATH_INFO) for viewer
  $relativeUrl = !preg_match("!^(/|\w+://)!", $detailUrl);
  if ($relativeUrl) {
    $thisDirUrl = dirname($_SERVER['SCRIPT_NAME']) ."/". $detailUrl;
    $thisDirUrl = preg_replace("|^[\\\\/]+|", "/", $thisDirUrl); // remove multiple leading slashes (and replace \ returned by dirname on windows in root)
    $link      .= $thisDirUrl;
  }
  else {
    $link .= $detailUrl;
  }


  // add url delimiter, filenameField, and record number
  $link .= $useSeoUrls ? '/' : '?';
  $link .= $filenameFieldValue;
  $link .= $recordNum;

  //
  return $link;
}

function getFilenameFieldValue($record, $filenameFields) {
  global $VIEWER_NAME;
  $filenameValue = "";

  // convert string to array
  if (!is_array($filenameFields)) {
    $filenameFields = preg_split("/\s*,\s*/", $filenameFields);
  }

  // error checking
  foreach ($filenameFields as $fieldname) {
    if ($fieldname == "") { continue; }
    if (!array_key_exists($fieldname, $record)) { die("$VIEWER_NAME: Unknown field '" .htmlspecialchars($fieldname). "' in filenameFields or titleField options!"); }
  }

  // get first defined field value
  foreach ($filenameFields as $fieldname) {
    if (@$record[$fieldname] == "") { continue; }
    $filenameValue = @$record[$fieldname];
    $filenameValue = preg_replace('/[^a-z0-9\.\-\_]+/i', '-', $filenameValue);
    $filenameValue = preg_replace("/(^-+|-+$)/", '', $filenameValue); # remove leading and trailing underscores
    if ($filenameValue) { $filenameValue .= "-"; }
    break;
  }

  //
  return $filenameValue;

}

// Note: This function is EXPERIMENTAL and may change in future
function getPrevAndNextRecords($options) {

  // error checking
  if (!@$options['tableName']) { $errors .= "No 'tableName' value specified in options!<br/>\n"; }
  if (!@$options['recordNum']) { $errors .= "No 'recordNum' value specified in options!<br/>\n"; }

  // load ALL records matching
  list($records, $metaData) = getRecords(array(
    'tableName'   => $options['tableName'],
  ));

  // get current record index
  for ($index=0; $index < count($records); $index++) {
    if ($records[$index]['num'] == $options['recordNum']) { break; }
  }

  // get first, prev, next, and last records
  $firstRecord = $records[0];
  $prevRecord  = $index                       ? $records[ $index-1 ] : array();
  $nextRecord  = $index < (count($records)-1) ? $records[ $index+1 ] : array();
  $lastRecord  = $records[ count($records) - 1];

  return array($prevRecord, $nextRecord, $firstRecord, $lastRecord);
}


// Note: This function is EXPERIMENTAL and may change in future
function incrementCounterField($tablename, $fieldname, $recordNumber) {
  global $VIEWER_NAME;

  // error checking
  if (!$tablename)    { die(__FUNCTION__ . ": No 'tablename' value specified!"); }
  if (!$fieldname)    { die(__FUNCTION__ . ": No 'fieldname' value specified!"); }
  if (!$recordNumber) { die(__FUNCTION__ . ": No 'recordNumber' value specified!"); }

  // update counter
  $escapedTableName = mysql_real_escape_string(getTableNameWithPrefix($tablename));
  $query  = "UPDATE `$escapedTableName` SET `$fieldname` = IFNULL(`$fieldname`,0) + 1";
  $query .= " WHERE `num` = " .mysql_real_escape_string($recordNumber);
  $result = @mysql_query($query);
  if (!$result) { die(__FUNCTION__ . " MySQL Error: ". htmlspecialchars(mysql_error()) . "\n"); }
  if (!mysql_affected_rows()) {
    die(__FUNCTION__ . ": Couldn't find record '" .htmlspecialchars($recordNumber). "'!");
  }

}

// return an array of list values
function getListValues($tableName, $fieldName, $fieldValue) {
  $array = explode("\t", $fieldValue);
  $array = array_slice($array, 1, -1); // remove blanks from leading/trailing tabs
  return $array;
}

// return an array of list labels
function getListLabels($tableName, $fieldName, $fieldValue) {
  $values = getListValues($tableName, $fieldName, $fieldValue);

  // load values to labels
  static $valuesToLabels;
  if (!@$valuesToLabels[$tableName][$fieldName]) {
    $valuesToLabels[$tableName][$fieldName] = getListOptions($tableName, $fieldName);
  }

  //
  $labels = array();
  foreach ($values as $value) {
    if (@$valuesToLabels[$tableName][$fieldName][$value]) {
      array_push($labels, $valuesToLabels[$tableName][$fieldName][$value]);
    }
    else {
      array_push($labels, $value);
    }
  }

  return $labels;
}



?>
