<?php

/***************************************************************************
 *
 * Copyright (c) by Andreas Unterkircher, unki@netshadow.at
 * All rights reserved
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 ***************************************************************************/

require_once "phpfspot_cfg.php";
require_once "phpfspot_db.php";

class PHPFSPOT {

   var $cfg;
   var $db;
   var $cfg_db;
   var $tmpl;
   var $tags;
   var $avail_tags;

   private $runtime_error = false;
   private $dbver;

   /**
    * class constructor
    *
    * this function will be called on class construct
    * and will check requirements, loads configuration,
    * open databases and start the user session
    */
   public function __construct()
   {
      $this->cfg = new PHPFSPOT_CFG;

      /* verify config settings */
      if($this->check_config_options()) {
         exit(1);
      }

      /* set application name and version information */
      $this->cfg->product = "phpfspot";
      $this->cfg->version = "1.3";

      $this->sort_orders= array(
         'date_asc' => 'Date &uarr;',
         'date_desc' => 'Date &darr;',
         'name_asc' => 'Name &uarr;',
         'name_desc' => 'Name &darr;',
         'tags_asc' => 'Tags &uarr;',
         'tags_desc' => 'Tags &darr;',
      );

      /* Check necessary requirements */
      if(!$this->checkRequirements()) {
         exit(1);
      }

      $this->db  = new PHPFSPOT_DB($this, $this->cfg->fspot_db);
      if(!is_writeable($this->cfg->fspot_db)) {
         print $this->cfg->fspot_db ." is not writeable for user ". $this->getuid() ."\n";
         exit(1);
      }

      $this->dbver = $this->getFspotDBVersion();

      if(!is_writeable(dirname($this->cfg->phpfspot_db))) {
         print dirname($this->cfg->phpfspot_db) .": directory is not writeable for user ". $this->getuid() ."\n";
         exit(1);
      }

      if(!is_writeable($this->cfg->base_path ."/templates_c")) {
         print $this->cfg->base_path ."/templates_c: directory is not writeable for user ". $this->getuid() ."\n";
         exit(1);
      }

      $this->cfg_db = new PHPFSPOT_DB($this, $this->cfg->phpfspot_db);
      if(!is_writeable($this->cfg->phpfspot_db)) {
         print $this->cfg->phpfspot_db ." is not writeable for user ". $this->getuid() ."\n";
         exit(1);
      }

      $this->check_config_table();

      /* include Smarty template engine */
      if(!$this->check_readable($this->cfg->smarty_path .'/libs/Smarty.class.php')) {
         exit(1);
      }
      require $this->cfg->smarty_path .'/libs/Smarty.class.php';
      /* overload Smarty class if our own template handler */
      require_once "phpfspot_tmpl.php";
      $this->tmpl = new PHPFSPOT_TMPL($this);

      /* check if all necessary indices exist */
      $this->checkDbIndices();

      /* if session is not yet started, do it now */
      if(session_id() == "")
         session_start();

      if(!isset($_SESSION['tag_condition']))
         $_SESSION['tag_condition'] = 'or';

      if(!isset($_SESSION['sort_order']))
         $_SESSION['sort_order'] = 'date_desc';

      if(!isset($_SESSION['searchfor_tag']))
         $_SESSION['searchfor_tag'] = '';

      // if begin_with is still set but thumbs_per_page is now 0, unset it
      if(isset($_SESSION['begin_with']) && $this->cfg->thumbs_per_page == 0)
         unset($_SESSION['begin_with']);

   } // __construct()

   public function __destruct()
   {

   } // __destruct()

   /**
    * show - generate html output
    *
    * this function can be called after the constructor has
    * prepared everyhing. it will load the index.tpl smarty
    * template. if necessary it will registere pre-selects
    * (photo index, photo, tag search, date search) into
    * users session.
    */
   public function show()
   {
      $this->tmpl->assign('searchfor_tag', $_SESSION['searchfor_tag']);
      $this->tmpl->assign('page_title', $this->cfg->page_title);
      $this->tmpl->assign('current_condition', $_SESSION['tag_condition']);
      $this->tmpl->assign('template_path', 'themes/'. $this->cfg->theme_name);

      if(isset($_GET['mode'])) {

         $_SESSION['start_action'] = $_GET['mode'];

         switch($_GET['mode']) {
            case 'showpi':
               if(isset($_GET['tags'])) {
                  $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
               }
               if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
                  $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
               }
               if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
                  $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
               }
               break;
            case 'showp':
               if(isset($_GET['tags'])) {
                  $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
                  $_SESSION['start_action'] = 'showp';
               }
               if(isset($_GET['id']) && is_numeric($_GET['id'])) {
                  $_SESSION['current_photo'] = $_GET['id'];
                  $_SESSION['start_action'] = 'showp';
               }
               if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
                  $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
               }
               if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
                  $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
               }
               break;
            case 'export':
               $this->tmpl->show("export.tpl");
               return;
               break;
            case 'slideshow':
               $this->tmpl->show("slideshow.tpl");
               return;
               break;
            case 'rss':
               if(isset($_GET['tags'])) {
                  $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
               }
               if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
                  $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
               }
               if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
                  $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
               }
               $this->getRSSFeed();
               return;
               break;
         }
      }

      if(isset($_SESSION['from_date']) && isset($_SESSION['to_date']))
         $this->tmpl->assign('date_search_enabled', true);

      $this->tmpl->register_function("sort_select_list", array(&$this, "smarty_sort_select_list"), false);
      $this->tmpl->assign('from_date', $this->get_calendar('from'));
      $this->tmpl->assign('to_date', $this->get_calendar('to'));
      $this->tmpl->assign('content_page', 'welcome.tpl');
      $this->tmpl->show("index.tpl");

   } // show()

   /**
    * get_tags - grab all tags of f-spot's database
    *
    * this function will get all available tags from
    * the f-spot database and store them within two
    * arrays within this class for later usage. in
    * fact, if the user requests (hide_tags) it will
    * opt-out some of them.
    *
    * this function is getting called once by show()
    */
   private function get_tags()
   {
      $this->avail_tags = Array();
      $count = 0;
   
      if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
         $query_str="
            SELECT
               DISTINCT t1.id as id, t1.name as name
            FROM  
               photo_tags pt1
            INNER JOIN photo_tags
               pt2 ON pt1.photo_id=pt2.photo_id
            INNER JOIN tags t1
               ON t1.id=pt1.tag_id
            INNER JOIN tags t2
               ON t2.id=pt2.tag_id
            WHERE
               t2.name IN  ('".implode("','",$this->cfg->show_tags)."')
            ORDER BY
               t1.sort_priority ASC";

         $result = $this->db->db_query($query_str);
      }
      else
      {
         $result = $this->db->db_query("
            SELECT id,name
            FROM tags
            ORDER BY sort_priority ASC
         ");
      }
      
      while($row = $this->db->db_fetch_object($result)) {

         $tag_id = $row['id'];
         $tag_name = $row['name'];

         /* if the user has specified to ignore this tag in phpfspot's
            configuration, ignore it here so it does not get added to
            the tag list.
         */
         if(in_array($row['name'], $this->cfg->hide_tags))
            continue;

         /* if you include the following if-clause and the user has specified
            to only show certain tags which are specified in phpfspot's
            configuration, ignore all others so they will not be added to the
            tag list.
         if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags) &&
            !in_array($row['name'], $this->cfg->show_tags))
            continue;
         */

         $this->tags[$tag_id] = $tag_name; 
         $this->avail_tags[$count] = $tag_id;
         $count++;

      }

   } // get_tags()

   /** 
    * extract all photo details
    * 
    * retrieve all available details from f-spot's
    * database and return them as object
    */
   public function get_photo_details($idx)
   {
      if($this->dbver < 9) {
         $query_str = "
            SELECT p.id, p.name, p.time, p.directory_path, p.description
            FROM photos p
         ";
      }
      else {
         $query_str = "
            SELECT p.id, p.uri, p.time, p.description
            FROM photos p
         ";
      }

      /* if show_tags is set, only return details for photos which
         are specified to be shown
      */
      if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
         $query_str.= "
            INNER JOIN photo_tags pt
               ON p.id=pt.photo_id
            INNER JOIN tags t
               ON pt.tag_id=t.id
            WHERE p.id='". $idx ."'
            AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
      }
      else {
         $query_str.= "
            WHERE p.id='". $idx ."'
         ";
      }

      if($result = $this->db->db_query($query_str)) {

         $row = $this->db->db_fetch_object($result);

         if($this->dbver < 9) {
            $row['uri'] = "file://". $row['directory_path'] ."/". $row['name'];
         }

         return $row;

      }
   
      return null;

   } // get_photo_details

   /**
    * returns aligned photo names 
    *
    * this function returns aligned (length) names for
    * an specific photo. If the length of the name exceeds
    * $limit the name will be shrinked (...)
    */
   public function getPhotoName($idx, $limit = 0)
   {
      if($details = $this->get_photo_details($idx)) {
         if($long_name = $this->parse_uri($details['uri'], 'filename')) {
            $name = $this->shrink_text($long_name, $limit);
            return $name;
         }
      }

      return null;

   } // getPhotoName()

   /**
    * shrink text according provided limit
    *
    * If the length of the name exceeds $limit the
    * text will be shortend and some content in between
    * will be replaced with "..." 
    */
   private function shrink_text($text, $limit)
   {
      if($limit != 0 && strlen($text) > $limit) {
         $text = substr($text, 0, $limit-5) ."...". substr($text, -($limit-5));
      }

      return $text;

   } // shrink_text();

   /**
    * translate f-spoth photo path
    * 
    * as the full-qualified path recorded in the f-spot database
    * is usally not the same as on the webserver, this function
    * will replace the path with that one specified in the cfg
    */
   public function translate_path($path, $width = 0)
   {  
      return str_replace($this->cfg->path_replace_from, $this->cfg->path_replace_to, $path);

   } // translate_path

   /**
    * control HTML ouput for a single photo
    *
    * this function provides all the necessary information
    * for the single photo template.
    */
   public function showPhoto($photo)
   {
      /* get all photos from the current photo selection */
      $all_photos = $this->getPhotoSelection();
      $count = count($all_photos);

      for($i = 0; $i < $count; $i++) {
         
         // $get_next will be set, when the photo which has to
         // be displayed has been found - this means that the
         // next available is in fact the NEXT image (for the
         // navigation icons) 
         if(isset($get_next)) {
            $next_img = $all_photos[$i];
            break;
         }

         /* the next photo is our NEXT photo */
         if($all_photos[$i] == $photo) {
            $get_next = 1;
         }
         else {
            $previous_img = $all_photos[$i];
         }

         if($photo == $all_photos[$i]) {
               $current = $i;
         }
      }

      $details = $this->get_photo_details($photo);

      if(!$details) {
         print "error";
         return;
      }

      $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
      $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo);

      if(!file_exists($orig_path)) {
         $this->_error("Photo ". $orig_path ." does not exist!<br />\n");
         return;
      }

      if(!is_readable($orig_path)) {
         $this->_error("Photo ". $orig_path ." is not readable for user ". $this->getuid() ."<br />\n");
         return;
      }

      /* If the thumbnail doesn't exist yet, try to create it */
      if(!file_exists($thumb_path)) {
         $this->gen_thumb($photo, true);
         $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo);
      }

      /* get f-spot database meta information */
      $meta = $this->get_meta_informations($orig_path);

      /* If EXIF data are available, use them */
      if(isset($meta['ExifImageWidth'])) {
         $meta_res = $meta['ExifImageWidth'] ."x". $meta['ExifImageLength'];
      } else {
         $info = getimagesize($orig_path);
         $meta_res = $info[0] ."x". $info[1]; 
      }

      $meta_date = isset($meta['FileDateTime']) ? strftime("%a %x %X", $meta['FileDateTime']) : "n/a";
      $meta_make = isset($meta['Make']) ? $meta['Make'] ." / ". $meta['Model'] : "n/a";
      $meta_size = isset($meta['FileSize']) ? round($meta['FileSize']/1024, 1) ."kbyte" : "n/a";

      $extern_link = "index.php?mode=showp&id=". $photo;
      $current_tags = $this->getCurrentTags();
      if($current_tags != "") {
         $extern_link.= "&tags=". $current_tags;
      }
      if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
         $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
      }

      $this->tmpl->assign('extern_link', $extern_link);

      if(!file_exists($thumb_path)) {
         $this->_error("Can't open file ". $thumb_path ."\n");
         return;
      }

      $info = getimagesize($thumb_path);

      $this->tmpl->assign('description', $details['description']);
      $this->tmpl->assign('image_name', $this->parse_uri($details['uri'], 'filename'));

      $this->tmpl->assign('width', $info[0]);
      $this->tmpl->assign('height', $info[1]);
      $this->tmpl->assign('ExifMadeOn', $meta_date);
      $this->tmpl->assign('ExifMadeWith', $meta_make);
      $this->tmpl->assign('ExifOrigResolution', $meta_res);
      $this->tmpl->assign('ExifFileSize', $meta_size);
 
      $this->tmpl->assign('image_url', 'phpfspot_img.php?idx='. $photo ."&amp;width=". $this->cfg->photo_width);
      $this->tmpl->assign('image_url_full', 'phpfspot_img.php?idx='. $photo);
      $this->tmpl->assign('image_filename', $this->parse_uri($details['uri'], 'filename'));

      $this->tmpl->assign('tags', $this->get_photo_tags($photo));
      $this->tmpl->assign('current', $current);

      if($previous_img) {
         $this->tmpl->assign('previous_url', "javascript:showImage(". $previous_img .");");
         $this->tmpl->assign('prev_img', $previous_img);
      }

      if($next_img) {
         $this->tmpl->assign('next_url', "javascript:showImage(". $next_img .");");
         $this->tmpl->assign('next_img', $next_img);
      }
      $this->tmpl->assign('mini_width', $this->cfg->mini_width);
      $this->tmpl->assign('photo_number', $i);
      $this->tmpl->assign('photo_count', count($all_photos));

      $this->tmpl->show("single_photo.tpl");

   } // showPhoto()

   /**
    * all available tags and tag cloud
    *
    * this function outputs all available tags (time ordered)
    * and in addition output them as tag cloud (tags which have
    * many photos will appears more then others)
    */
   public function getAvailableTags()
   {
      /* retrive tags from database */
      $this->get_tags();

      $output = "";

      $result = $this->db->db_query("
         SELECT tag_id as id, count(tag_id) as quantity
         FROM photo_tags
         INNER JOIN tags t
            ON t.id = tag_id
         GROUP BY tag_id
         ORDER BY t.name ASC
      ");

      $tags = Array();

      while($row = $this->db->db_fetch_object($result)) {
         $tags[$row['id']] = $row['quantity'];
      }

      // change these font sizes if you will
      $max_size = 125; // max font size in %
      $min_size = 75; // min font size in %

      // get the largest and smallest array values
      $max_qty = max(array_values($tags));
      $min_qty = min(array_values($tags));

      // find the range of values
      $spread = $max_qty - $min_qty;
      if (0 == $spread) { // we don't want to divide by zero
         $spread = 1;
      }

      // determine the font-size increment
      // this is the increase per tag quantity (times used)
      $step = ($max_size - $min_size)/($spread);

      // loop through our tag array
      foreach ($tags as $key => $value) {

         if(isset($_SESSION['selected_tags']) && in_array($key, $_SESSION['selected_tags']))
            continue;

         // calculate CSS font-size
         // find the $value in excess of $min_qty
         // multiply by the font-size increment ($size)
         // and add the $min_size set above
         $size = $min_size + (($value - $min_qty) * $step);
          // uncomment if you want sizes in whole %:
         $size = ceil($size);

         if(isset($this->tags[$key])) {
            $output.= "<a href=\"javascript:Tags('add', ". $key .");\" class=\"tag\" style=\"font-size: ". $size ."%;\">". $this->tags[$key] ."</a>, ";
         }

      }

      $output = substr($output, 0, strlen($output)-2);
      print $output;

   } // getAvailableTags()

   /**
    * output all selected tags
    *
    * this function output all tags which have been selected
    * by the user. the selected tags are stored in the 
    * session-variable $_SESSION['selected_tags']
    */
   public function getSelectedTags()
   {
      /* retrive tags from database */
      $this->get_tags();

      $output = "";

      foreach($this->avail_tags as $tag)
      {
         // return all selected tags
         if(isset($_SESSION['selected_tags']) && in_array($tag, $_SESSION['selected_tags'])) {
            $output.= "<a href=\"javascript:Tags('del', ". $tag .");\" class=\"tag\">". $this->tags[$tag] ."</a>, ";
         }
      }

      if($output != "") {
         $output = substr($output, 0, strlen($output)-2);
         return $output;
      }
      else {
         return "no tags selected";
      }

   } // getSelectedTags()

   /**
    * add tag to users session variable
    *
    * this function will add the specified to users current
    * tag selection. if a date search has been made before
    * it will be now cleared
    */
   public function addTag($tag)
   {
      if(!isset($_SESSION['selected_tags']))
         $_SESSION['selected_tags'] = Array();

      if(isset($_SESSION['searchfor_tag']))
         unset($_SESSION['searchfor_tag']);

      if(!in_array($tag, $_SESSION['selected_tags']))
         array_push($_SESSION['selected_tags'], $tag);


      return "ok";
   
   } // addTag()

   /**
    * remove tag to users session variable
    *
    * this function removes the specified tag from
    * users current tag selection
    */
   public function delTag($tag)
   {
      if(isset($_SESSION['searchfor_tag']))
         unset($_SESSION['searchfor_tag']);

      if(isset($_SESSION['selected_tags'])) {
         $key = array_search($tag, $_SESSION['selected_tags']);
         unset($_SESSION['selected_tags'][$key]);
         sort($_SESSION['selected_tags']);
      }

      return "ok";

   } // delTag()

   /**
    * reset tag selection
    *
    * if there is any tag selection, it will be
    * deleted now
    */
   public function resetTags()
   {
      if(isset($_SESSION['selected_tags']))
         unset($_SESSION['selected_tags']);

   } // resetTags()

   /**
    * reset single photo
    *
    * if a specific photo was requested (external link)
    * unset the session variable now
    */
   public function resetPhotoView()
   {
      if(isset($_SESSION['current_photo']))
         unset($_SESSION['current_photo']);

   } // resetPhotoView();

   /**
    * reset tag search
    *
    * if any tag search has taken place, reset it now
    */
   public function resetTagSearch()
   {
      if(isset($_SESSION['searchfor_tag']))
         unset($_SESSION['searchfor_tag']);

   } // resetTagSearch()

   /**
    * reset name search
    *
    * if any name search has taken place, reset it now
    */
   public function resetNameSearch()
   {
      if(isset($_SESSION['searchfor_name']))
         unset($_SESSION['searchfor_name']);

   } // resetNameSearch()

   /**
    * reset date search
    *
    * if any date search has taken place, reset
    * it now
    */
   public function resetDateSearch()
   {
      if(isset($_SESSION['from_date']))
         unset($_SESSION['from_date']);
      if(isset($_SESSION['to_date']))
         unset($_SESSION['to_date']);

   } // resetDateSearch();

   /**
    * return all photo according selection
    *
    * this function returns all photos based on
    * the tag-selection, tag- or date-search.
    * the tag-search also has to take care of AND
    * and OR conjunctions
    */
   public function getPhotoSelection()
   {  
      $matched_photos = Array();

      if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
         $from_date = $_SESSION['from_date'];
         $to_date = $_SESSION['to_date'];
         $additional_where_cond = "
               p.time>='". $from_date ."'
            AND
               p.time<='". $to_date ."'
         ";
      } 

      if(isset($_SESSION['searchfor_name'])) {
         if($this->dbver < 9) {
            $additional_where_cond.= "
                  (
                        p.name LIKE '%". $_SESSION['searchfor_name'] ."%'
                     OR
                        p.description LIKE '%". $_SESSION['searchfor_name'] ."%'
                  )
            ";
         }
         else {
            $additional_where_cond.= "
                  (
                        basename(p.uri) LIKE '%". $_SESSION['searchfor_name'] ."%'
                     OR
                        p.description LIKE '%". $_SESSION['searchfor_name'] ."%'
                  )
            ";
         }
      }

      if(isset($_SESSION['sort_order'])) {
         $order_str = $this->get_sort_order();
      }

      /* return a search result */
      if(isset($_SESSION['searchfor_tag']) && $_SESSION['searchfor_tag'] != '') {
         $query_str = "
            SELECT DISTINCT pt1.photo_id
               FROM photo_tags pt1
            INNER JOIN photo_tags pt2
               ON pt1.photo_id=pt2.photo_id
            INNER JOIN tags t
               ON pt1.tag_id=t.id
            INNER JOIN photos p
               ON pt1.photo_id=p.id
            INNER JOIN tags t2
               ON pt2.tag_id=t2.id
            WHERE t.name LIKE '%". $_SESSION['searchfor_tag'] ."%' ";

         if(isset($additional_where_cond))
            $query_str.= "AND ". $additional_where_cond ." ";

         if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
            $query_str.= "AND t2.name IN ('".implode("','",$this->cfg->show_tags)."')";
         }
         
         if(isset($order_str))
            $query_str.= $order_str;

         $result = $this->db->db_query($query_str);
         while($row = $this->db->db_fetch_object($result)) {
            array_push($matched_photos, $row['photo_id']);
         }
         return $matched_photos;
      }

      /* return according the selected tags */
      if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
         $selected = "";
         foreach($_SESSION['selected_tags'] as $tag)
            $selected.= $tag .",";
         $selected = substr($selected, 0, strlen($selected)-1);

         /* photo has to match at least on of the selected tags */
         if($_SESSION['tag_condition'] == 'or') {
            $query_str = "
               SELECT DISTINCT pt1.photo_id
                  FROM photo_tags pt1
               INNER JOIN photo_tags pt2
                  ON pt1.photo_id=pt2.photo_id
               INNER JOIN tags t
                  ON pt2.tag_id=t.id
               INNER JOIN photos p
                  ON pt1.photo_id=p.id
               WHERE pt1.tag_id IN (". $selected .")
            ";
            if(isset($additional_where_cond)) 
               $query_str.= "AND ". $additional_where_cond ." ";

            if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
               $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
            }

            if(isset($order_str))
               $query_str.= $order_str;
         }
         /* photo has to match all selected tags */
         elseif($_SESSION['tag_condition'] == 'and') {

            if(count($_SESSION['selected_tags']) >= 32) {
               print "A SQLite limit of 32 tables within a JOIN SELECT avoids to<br />\n";
               print "evaluate your tag selection. Please remove some tags from your selection.\n";
               return Array();
            } 

            /* Join together a table looking like

               pt1.photo_id pt1.tag_id pt2.photo_id pt2.tag_id ...

               so the query can quickly return all images matching the
               selected tags in an AND condition

            */

            $query_str = "
               SELECT DISTINCT pt1.photo_id
                  FROM photo_tags pt1
            ";

            if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
               $query_str.= "
                  INNER JOIN tags t
                     ON pt1.tag_id=t.id
               ";
            }

            for($i = 0; $i < count($_SESSION['selected_tags']); $i++) {
               $query_str.= "
                  INNER JOIN photo_tags pt". ($i+2) ."
                     ON pt1.photo_id=pt". ($i+2) .".photo_id
               ";
            }
            $query_str.= "
               INNER JOIN photos p
                  ON pt1.photo_id=p.id
            ";
            $query_str.= "WHERE pt2.tag_id=". $_SESSION['selected_tags'][0]." ";
            for($i = 1; $i < count($_SESSION['selected_tags']); $i++) {
               $query_str.= "
                  AND pt". ($i+2) .".tag_id=". $_SESSION['selected_tags'][$i] ."
               "; 
            }
            if(isset($additional_where_cond)) 
               $query_str.= "AND ". $additional_where_cond;

            if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
               $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
            }

            if(isset($order_str))
               $query_str.= $order_str;

         }

         $result = $this->db->db_query($query_str);
         while($row = $this->db->db_fetch_object($result)) {
            array_push($matched_photos, $row['photo_id']);
         }
         return $matched_photos;
      }

      /* return all available photos */
      $query_str = "
         SELECT DISTINCT p.id
         FROM photos p
         LEFT JOIN photo_tags pt
            ON p.id=pt.photo_id
         LEFT JOIN tags t
            ON pt.tag_id=t.id
      ";

      if(isset($additional_where_cond)) 
         $query_str.= "WHERE ". $additional_where_cond ." ";

      if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
         if(isset($additional_where_cond))
            $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
         else
            $query_str.= "WHERE t.name IN ('".implode("','",$this->cfg->show_tags). "')";
      }
 
      if(isset($order_str))
         $query_str.= $order_str;

      $result = $this->db->db_query($query_str);
      while($row = $this->db->db_fetch_object($result)) {
         array_push($matched_photos, $row['id']);
      }
      return $matched_photos;

   } // getPhotoSelection()

    /**
    * control HTML ouput for photo index
    *
    * this function provides all the necessary information
    * for the photo index template.
    */
   public function showPhotoIndex()
   {
      $photos = $this->getPhotoSelection();

      $count = count($photos);

      if(isset($_SESSION['begin_with']) && $_SESSION['begin_with'] != "")
         $anchor = $_SESSION['begin_with'];

      if(!isset($this->cfg->thumbs_per_page) || $this->cfg->thumbs_per_page == 0) {

         $begin_with = 0;
         $end_with = $count;

      }
      elseif($this->cfg->thumbs_per_page > 0) {

         if(!isset($_SESSION['begin_with']) || $_SESSION['begin_with'] == 0) {
            $begin_with = 0;
         }
         else {
            $begin_with = $_SESSION['begin_with'];
         }

         $end_with = $begin_with + $this->cfg->thumbs_per_page;
      }

      $thumbs = 0;
      $images[$thumbs] = Array();
      $img_height[$thumbs] = Array();
      $img_width[$thumbs] = Array();
      $img_id[$thumbs] = Array();
      $img_name[$thumbs] = Array();
      $img_title = Array();

      for($i = $begin_with; $i < $end_with; $i++) {

         if(isset($photos[$i])) {

            $images[$thumbs] = $photos[$i];
            $img_id[$thumbs] = $i;
            $img_name[$thumbs] = htmlspecialchars($this->getPhotoName($photos[$i], 15));
            $img_title[$thumbs] = "Click to view photo ". htmlspecialchars($this->getPhotoName($photos[$i], 0));

            $thumb_path = $this->get_thumb_path($this->cfg->thumb_width, $photos[$i]);

            if(file_exists($thumb_path)) {
               $info = getimagesize($thumb_path); 
               $img_width[$thumbs] = $info[0];
               $img_height[$thumbs] = $info[1];
            }
            $thumbs++;
         } 
      }

      // +1 for for smarty's selection iteration
      $thumbs++;

      if(isset($_SESSION['searchfor_tag']) && $_SESSION['searchfor_tag'] != '')
         $this->tmpl->assign('searchfor_tag', $_SESSION['searchfor_tag']);

      if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
         $this->tmpl->assign('from_date', $this->ts2str($_SESSION['from_date']));
         $this->tmpl->assign('to_date', $this->ts2str($_SESSION['to_date']));
      }

      if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
         $this->tmpl->assign('tag_result', 1);
      }

      /* do we have to display the page selector ? */
      if($this->cfg->thumbs_per_page != 0) {

         $page_select = "";
      
         /* calculate the page switchers */
         $previous_start = $begin_with - $this->cfg->thumbs_per_page;
         $next_start = $begin_with + $this->cfg->thumbs_per_page;

         if($begin_with != 0) 
            $this->tmpl->assign("previous_url", "javascript:showPhotoIndex(". $previous_start .");"); 
         if($end_with < $count)
            $this->tmpl->assign("next_url", "javascript:showPhotoIndex(". $next_start .");"); 

         $photo_per_page  = $this->cfg->thumbs_per_page;
         $last_page = ceil($count / $photo_per_page);

         /* get the current selected page */
         if($begin_with == 0) {
            $current_page = 1;
         } else {
            $current_page = 0;
            for($i = $begin_with; $i >= 0; $i-=$photo_per_page) {
               $current_page++;
            }
         } 

         $dotdot_made = 0;

         for($i = 1; $i <= $last_page; $i++) {

            if($current_page == $i)
               $style = "style=\"font-size: 125%; text-decoration: underline;\"";
            elseif($current_page-1 == $i || $current_page+1 == $i)
               $style = "style=\"font-size: 105%;\"";
            elseif(($current_page-5 >= $i) && ($i != 1) ||
               ($current_page+5 <= $i) && ($i != $last_page))
               $style = "style=\"font-size: 75%;\"";
            else
               $style = "";

            $select = "<a href=\"javascript:showPhotoIndex(". (($i*$photo_per_page)-$photo_per_page) .");\"";
               if($style != "")
                  $select.= $style;
            $select.= ">". $i ."</a>&nbsp;";

            // until 9 pages we show the selector from 1-9
            if($last_page <= 9) {
               $page_select.= $select;
               continue;
            } else {
               if($i == 1 /* first page */ || 
                  $i == $last_page /* last page */ ||
                  $i == $current_page /* current page */ ||
                  $i == ceil($last_page * 0.25) /* first quater */ ||
                  $i == ceil($last_page * 0.5) /* half */ ||
                  $i == ceil($last_page * 0.75) /* third quater */ ||
                  (in_array($i, array(1,2,3,4,5,6)) && $current_page <= 4) /* the first 6 */ ||
                  (in_array($i, array($last_page, $last_page-1, $last_page-2, $last_page-3, $last_page-4, $last_page-5)) && $current_page >= $last_page-4) /* the last 6 */ ||
                  $i == $current_page-3 || $i == $current_page-2 || $i == $current_page-1 /* three before */ ||
                  $i == $current_page+3 || $i == $current_page+2 || $i == $current_page+1 /* three after */) {

                  $page_select.= $select;
                  $dotdot_made = 0;
                  continue;

               }
            }

            if(!$dotdot_made) {
               $page_select.= ".........&nbsp;";
               $dotdot_made = 1;
            }
         }

         /* only show the page selector if we have more then one page */
         if($last_page > 1)
            $this->tmpl->assign('page_selector', $page_select);
      }

      
      $current_tags = $this->getCurrentTags();
      $extern_link = "index.php?mode=showpi";
      $rss_link = "index.php?mode=rss";
      if($current_tags != "") {
         $extern_link.= "&tags=". $current_tags;
         $rss_link.= "&tags=". $current_tags;
      }
      if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
         $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
         $rss_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
      }

      $export_link = "index.php?mode=export";
      $slideshow_link = "index.php?mode=slideshow";

      $this->tmpl->assign('extern_link', $extern_link);
      $this->tmpl->assign('slideshow_link', $slideshow_link);
      $this->tmpl->assign('export_link', $export_link);
      $this->tmpl->assign('rss_link', $rss_link);
      $this->tmpl->assign('count', $count);
      $this->tmpl->assign('width', $this->cfg->thumb_width);
      $this->tmpl->assign('thumb_container_width', $this->cfg->thumb_width);
      $this->tmpl->assign('thumb_container_height', $this->cfg->thumb_height+20);
      $this->tmpl->assign('images', $images);
      $this->tmpl->assign('img_width', $img_width);
      $this->tmpl->assign('img_height', $img_height);
      $this->tmpl->assign('img_id', $img_id);
      $this->tmpl->assign('img_name', $img_name);
      $this->tmpl->assign('img_title', $img_title);
      $this->tmpl->assign('thumbs', $thumbs);

      $this->tmpl->show("photo_index.tpl");

      if(isset($anchor))
         print "<script language=\"JavaScript\">self.location.hash = '#image". $anchor ."';</script>\n";

   } // showPhotoIndex()

   /**
    * show credit template
    */
   public function showCredits()
   {
      $this->tmpl->assign('version', $this->cfg->version);
      $this->tmpl->assign('product', $this->cfg->product);
      $this->tmpl->assign('db_version', $this->dbver);
      $this->tmpl->show("credits.tpl");

   } // showCredits()

   /**
    * create_thumbnails for the requested width
    *
    * this function creates image thumbnails of $orig_image
    * stored as $thumb_image. It will check if the image is
    * in a supported format, if necessary rotate the image
    * (based on EXIF orientation meta headers) and re-sizing.
    */
   public function create_thumbnail($orig_image, $thumb_image, $width)
   {  
      if(!file_exists($orig_image)) {
         return false;
      }

      $details = getimagesize($orig_image);
      
      /* check if original photo is a support image type */
      if(!$this->checkifImageSupported($details['mime']))
         return false;

      $meta = $this->get_meta_informations($orig_image);

      $rotate = 0;
      $flip_hori = false;
      $flip_vert = false;

      switch($meta['Orientation']) {
         case 1: /* top, left */
            /* nothing to do */ break;
         case 2: /* top, right */
            $rotate = 0; $flip_hori = true; break;
         case 3: /* bottom, left */
            $rotate = 180; break;
         case 4: /* bottom, right */
            $flip_vert = true; break;
         case 5: /* left side, top */
            $rotate = 90; $flip_vert = true; break;
         case 6: /* right side, top */
            $rotate = 90; break;
         case 7: /* left side, bottom */
            $rotate = 270; $flip_vert = true; break;
         case 8: /* right side, bottom */
            $rotate = 270; break;
      }

      $src_img = @imagecreatefromjpeg($orig_image);

      if(!$src_img) {
         print "Can't load image from ". $orig_image ."\n";
         return false;
      }

      /* grabs the height and width */
      $cur_width = imagesx($src_img);
      $cur_height = imagesy($src_img);

      // If requested width is more then the actual image width,
      // do not generate a thumbnail, instead safe the original
      // as thumbnail but with lower quality. But if the image
      // is to heigh too, then we still have to resize it.
      if($width >= $cur_width && $cur_height < $this->cfg->thumb_height) {
         $result = imagejpeg($src_img, $thumb_image, 75);
         imagedestroy($src_img);
         return true;
      }

      // If the image will be rotate because EXIF orientation said so
      // 'virtually rotate' the image for further calculations
      if($rotate == 90 || $rotate == 270) {
         $tmp = $cur_width;
         $cur_width = $cur_height;
         $cur_height = $tmp;
      }

      /* calculates aspect ratio */
      $aspect_ratio = $cur_height / $cur_width;

      /* sets new size */
      if($aspect_ratio < 1) {
         $new_w = $width;
         $new_h = abs($new_w * $aspect_ratio);
      } else {
         /* 'virtually' rotate the image and calculate it's ratio */
         $tmp_w = $cur_height;
         $tmp_h = $cur_width;
         /* now get the ratio from the 'rotated' image */
         $tmp_ratio = $tmp_h/$tmp_w;
         /* now calculate the new dimensions */
         $tmp_w = $width;
         $tmp_h = abs($tmp_w * $tmp_ratio);

         // now that we know, how high they photo should be, if it
         // gets rotated, use this high to scale the image
         $new_h = $tmp_h;
         $new_w = abs($new_h / $aspect_ratio);

         // If the image will be rotate because EXIF orientation said so
         // now 'virtually rotate' back the image for the image manipulation
         if($rotate == 90 || $rotate == 270) {
            $tmp = $new_w;
            $new_w = $new_h;
            $new_h = $tmp;
         }
      }

      /* creates new image of that size */
      $dst_img = imagecreatetruecolor($new_w, $new_h);

      imagefill($dst_img, 0, 0, ImageColorAllocate($dst_img, 255, 255, 255));

      /* copies resized portion of original image into new image */
      imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, imagesx($src_img), imagesy($src_img));

      /* needs the image to be flipped horizontal? */
      if($flip_hori) {
         $this->_debug("(FLIP)");
         $dst_img = $this->flipImage($dst_img, 'hori');
      }
      /* needs the image to be flipped vertical? */
      if($flip_vert) {
         $this->_debug("(FLIP)");
         $dst_img = $this->flipImage($dst_img, 'vert');
      }

      if($rotate) {
         $this->_debug("(ROTATE)");
         $dst_img = $this->rotateImage($dst_img, $rotate);
      }

      /* write down new generated file */
      $result = imagejpeg($dst_img, $thumb_image, 75);

      /* free your mind */
      imagedestroy($dst_img);
      imagedestroy($src_img);

      if($result === false) {
         print "Can't write thumbnail ". $thumb_image ."\n";
         return false;
      }

      return true;

   } // create_thumbnail()

   /**
    * return all exif meta data from the file
    */
   public function get_meta_informations($file)
   {
      return exif_read_data($file);

   } // get_meta_informations()

   /**
    * create phpfspot own sqlite database
    *
    * this function creates phpfspots own sqlite database
    * if it does not exist yet. this own is used to store
    * some necessary informations (md5 sum's, ...).
    */
   public function check_config_table()
   {
      // if the config table doesn't exist yet, create it
      if(!$this->cfg_db->db_check_table_exists("images")) {
         $this->cfg_db->db_exec("
            CREATE TABLE images (
               img_idx int primary key,
               img_md5 varchar(32)
            )
            ");
      }

   } // check_config_table

   /**
    * Generates a thumbnail from photo idx
    *
    * This function will generate JPEG thumbnails from provided F-Spot photo
    * indizes.
    *
    * 1. Check if all thumbnail generations (width) are already in place and
    *    readable
    * 2. Check if the md5sum of the original file has changed
    * 3. Generate the thumbnails if needed
    */
   public function gen_thumb($idx = 0, $force = 0, $overwrite = false)
   {
      $error = 0;

      $resolutions = Array(
         $this->cfg->thumb_width,
         $this->cfg->photo_width,
         $this->cfg->mini_width,
      );

      /* get details from F-Spot's database */
      $details = $this->get_photo_details($idx);

      /* calculate file MD5 sum */
      $full_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));

      if(!file_exists($full_path)) {
         $this->_error("File ". $full_path ." does not exist\n");
         return;
      }

      if(!is_readable($full_path)) {
         $this->_error("File ". $full_path ." is not readable for ". $this->getuid() ."\n");
         return;
      }

      $file_md5 = md5_file($full_path);

      $this->_debug("Image [". $idx ."] ". $this->shrink_text($this->parse_uri($details['uri'], 'filename'), 20) ." Thumbnails:");

      $changes = false;

      foreach($resolutions as $resolution) {
   
         $generate_it = false;

         $thumb_sub_path = substr($file_md5, 0, 2);
         $thumb_path = $this->cfg->thumb_path ."/". $thumb_sub_path ."/". $resolution ."_". $file_md5;

         if(!file_exists(dirname($thumb_path))) {
            mkdir(dirname($thumb_path), 0755);
         }

         /* if the thumbnail file doesn't exist, create it */
         if(!file_exists($thumb_path)) {
            $generate_it = true;
         }
         /* if the file hasn't changed there is no need to regen the thumb */
         elseif($file_md5 != $this->getMD5($idx) || $force) {
            $generate_it = true;
         }

         if($generate_it || $overwrite) {

            $this->_debug(" ". $resolution ."px");
            if(!$this->create_thumbnail($full_path, $thumb_path, $resolution))
               $error = 1;

            $changes = true;
         }
      }

      if(!$changes) {
         $this->_debug(" already exist");
      }

      /* set the new/changed MD5 sum for the current photo */
      if(!$error) {
         $this->setMD5($idx, $file_md5);
      }

      $this->_debug("\n");

   } // gen_thumb()

   /**
    * returns stored md5 sum for a specific photo
    *
    * this function queries the phpfspot database for a
    * stored MD5 checksum of the specified photo
    */
   public function getMD5($idx)
   {
      $result = $this->cfg_db->db_query("
         SELECT img_md5 
         FROM images
         WHERE img_idx='". $idx ."'
      ");

      if(!$result)
         return 0;

      $img = $this->cfg_db->db_fetch_object($result);
      return $img['img_md5'];
      
   } // getMD5()

   /**
    * set MD5 sum for the specific photo
    */
   private function setMD5($idx, $md5)
   {
      $result = $this->cfg_db->db_exec("
         REPLACE INTO images (img_idx, img_md5)
         VALUES ('". $idx ."', '". $md5 ."')
      ");

   } // setMD5()

   /**
    * store current tag condition
    *
    * this function stores the current tag condition
    * (AND or OR) in the users session variables
    */
   public function setTagCondition($mode)
   {
      $_SESSION['tag_condition'] = $mode;

      return "ok";

   } // setTagCondition()

   /** 
    * invoke tag & date search 
    *
    * this function will return all matching tags and store
    * them in the session variable selected_tags. furthermore
    * it also handles the date search.
    * getPhotoSelection() will then only return the matching
    * photos.
    */
   public function startSearch($searchfor_tag, $from = 0, $to = 0)
   {
      if(isset($_POST['from']) && $this->isValidDate($_POST['from'])) {
         $from = $_POST['from'];
      }
      if(isset($_POST['to']) && $this->isValidDate($_POST['to'])) {
         $to = $_POST['to'];
      }

      if(isset($_POST['for_tag']) && is_string($_POST['for_tag'])) {
         $searchfor_tag = $_POST['for_tag'];
      }

      if(isset($_POST['for_name']) && is_string($_POST['for_name'])) {
         $searchfor_name = $_POST['for_name'];
      }

      $this->get_tags();

      $_SESSION['searchfor_tag'] = $searchfor_tag;
      $_SESSION['searchfor_name'] = $searchfor_name;

      if($from != 0)
         $_SESSION['from_date'] = strtotime($from ." 00:00:00");
      else
         unset($_SESSION['from_date']);

      if($to != 0)
         $_SESSION['to_date'] = strtotime($to ." 23:59:59");
      else
         unset($_SESSION['to_date']);

      if($searchfor_tag != "") {
         /* new search, reset the current selected tags */
         $_SESSION['selected_tags'] = Array();
         foreach($this->avail_tags as $tag) {
            if(preg_match('/'. $searchfor_tag .'/i', $this->tags[$tag]))
               array_push($_SESSION['selected_tags'], $tag);
         }
      }

      return "ok";

   } // startSearch()

   /**
    * updates sort order in session variable
    *
    * this function is invoked by RPC and will sort the requested
    * sort order in the session variable.
    */
   public function updateSortOrder($order)
   {
      if(isset($this->sort_orders[$order])) {
         $_SESSION['sort_order'] = $order;
         return "ok";
      }

      return "unkown error";

   } // updateSortOrder()

   /**
    * rotate image
    *
    * this function rotates the image according the
    * specified angel.
    */
   private function rotateImage($img, $degrees)
   {
      if(function_exists("imagerotate")) {
         $img = imagerotate($img, $degrees, 0);
      } else {
         function imagerotate($src_img, $angle)
         {
            $src_x = imagesx($src_img);
            $src_y = imagesy($src_img);
            if ($angle == 180) {
               $dest_x = $src_x;
               $dest_y = $src_y;
            }
            elseif ($src_x <= $src_y) {
               $dest_x = $src_y;
               $dest_y = $src_x;
            }
            elseif ($src_x >= $src_y) {
               $dest_x = $src_y;
               $dest_y = $src_x;
            }
               
            $rotate=imagecreatetruecolor($dest_x,$dest_y);
            imagealphablending($rotate, false);
               
            switch ($angle) {
            
               case 90:
                  for ($y = 0; $y < ($src_y); $y++) {
                     for ($x = 0; $x < ($src_x); $x++) {
                        $color = imagecolorat($src_img, $x, $y);
                        imagesetpixel($rotate, $dest_x - $y - 1, $x, $color);
                     }
                  }
                  break;

               case 270:
                  for ($y = 0; $y < ($src_y); $y++) {
                     for ($x = 0; $x < ($src_x); $x++) {
                        $color = imagecolorat($src_img, $x, $y);
                        imagesetpixel($rotate, $y, $dest_y - $x - 1, $color);
                     }
                  }
                  break;

               case 180:
                  for ($y = 0; $y < ($src_y); $y++) {
                     for ($x = 0; $x < ($src_x); $x++) {
                        $color = imagecolorat($src_img, $x, $y);
                        imagesetpixel($rotate, $dest_x - $x - 1, $dest_y - $y - 1, $color);
                     }
                  }
                  break;

               default:
                  $rotate = $src_img;
                  break;
            };

            return $rotate;

         }

         $img = imagerotate($img, $degrees);

      }

      return $img;

   } // rotateImage()

   /**
    * returns flipped image
    *
    * this function will return an either horizontal or
    * vertical flipped truecolor image.
    */
   private function flipImage($image, $mode)
   {
      $w = imagesx($image);
      $h = imagesy($image);
      $flipped = imagecreatetruecolor($w, $h);

      switch($mode) {
         case 'vert':
            for ($y = 0; $y < $h; $y++) {
               imagecopy($flipped, $image, 0, $y, 0, $h - $y - 1, $w, 1);
            }
            break;
         case 'hori':
            for ($x = 0; $x < $w; $x++) {
               imagecopy($flipped, $image, $x, 0, $w - $x - 1, 0, 1, $h);
            }
            break;
      }

      return $flipped;

   } // flipImage()

   /**
    * return all assigned tags for the specified photo
    */
   private function get_photo_tags($idx)
   {
      $result = $this->db->db_query("
         SELECT t.id, t.name
         FROM tags t
         INNER JOIN photo_tags pt
            ON t.id=pt.tag_id
         WHERE pt.photo_id='". $idx ."'
      ");

      $tags = Array();

      while($row = $this->db->db_fetch_object($result))
         $tags[$row['id']] = $row['name'];

      return $tags;

   } // get_photo_tags()

   /**
    * create on-the-fly images with text within
    */
   public function showTextImage($txt, $color=000000, $space=4, $font=4, $w=300)
   {
      if (strlen($color) != 6) 
         $color = 000000;

      $int = hexdec($color);
      $h = imagefontheight($font);
      $fw = imagefontwidth($font);
      $txt = explode("\n", wordwrap($txt, ($w / $fw), "\n"));
      $lines = count($txt);
      $im = imagecreate($w, (($h * $lines) + ($lines * $space)));
      $bg = imagecolorallocate($im, 255, 255, 255);
      $color = imagecolorallocate($im, 0xFF & ($int >> 0x10), 0xFF & ($int >> 0x8), 0xFF & $int);
      $y = 0;

      foreach ($txt as $text) {
         $x = (($w - ($fw * strlen($text))) / 2);
         imagestring($im, $font, $x, $y, $text, $color);
         $y += ($h + $space);
      }

      Header("Content-type: image/png");
      ImagePng($im);

   } // showTextImage()

   /**
    * check if all requirements are met
    */
   private function checkRequirements()
   {
      if(!function_exists("imagecreatefromjpeg")) {
         print "PHP GD library extension is missing<br />\n";
         $missing = true;
      }

      if($this->cfg->db_access == "native" && !function_exists("sqlite3_open")) {
         print "PHP SQLite3 library extension is missing<br />\n";
         $missing = true;
      }

      /* Check for HTML_AJAX PEAR package, lent from Horde project */
      ini_set('track_errors', 1);
      @include_once 'HTML/AJAX/Server.php';
      if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
         print "PEAR HTML_AJAX package is missing<br />\n";
         $missing = true;
      }
      @include_once 'Calendar/Calendar.php';
      if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
         print "PEAR Calendar package is missing<br />\n";
         $missing = true;
      }
      @include_once 'Console/Getopt.php';
      if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
         print "PEAR Console_Getopt package is missing<br />\n";
         $missing = true;
      }
      ini_restore('track_errors');

      if(isset($missing))
         return false;

      return true;

   } // checkRequirements()

   private function _debug($text)
   {
      if($this->fromcmd) {
         print $text;
      }

   } // _debug()

   /**
    * check if specified MIME type is supported
    */
   public function checkifImageSupported($mime)
   {
      if(in_array($mime, Array("image/jpeg")))
         return true;

      return false;

   } // checkifImageSupported()

   public function _error($text)
   {
      switch($this->cfg->logging) {
         default:
         case 'display':
            print "<img src=\"resources/green_info.png\" alt=\"warning\" />\n";
            print $text ."<br />\n";
            break;
         case 'errorlog':  
            error_log($text);
            break;
         case 'logfile':
            error_log($text, 3, $his->cfg->log_file);
            break;
      }

      $this->runtime_error = true;

   } // _error()

   /**
    * output calendard input fields
    */
   private function get_calendar($mode)
   {
      $year = isset($_SESSION[$mode .'_date']) ? date("Y", $_SESSION[$mode .'_date']) : date("Y");
      $month = isset($_SESSION[$mode .'_date']) ? date("m", $_SESSION[$mode .'_date']) : date("m");
      $day = isset($_SESSION[$mode .'_date']) ? date("d", $_SESSION[$mode .'_date']) : date("d");

      $output = "<input type=\"text\" size=\"3\" id=\"". $mode ."year\" value=\"". $year ."\"";
      if(!isset($_SESSION[$mode .'_date']))
         $output.= " disabled=\"disabled\"";
      $output.= " />\n";
      $output.= "<input type=\"text\" size=\"1\" id=\"". $mode ."month\" value=\"". $month ."\"";
      if(!isset($_SESSION[$mode .'_date']))
         $output.= " disabled=\"disabled\"";
      $output.= " />\n";
      $output.= "<input type=\"text\" size=\"1\" id=\"". $mode ."day\" value=\"". $day ."\"";
      if(!isset($_SESSION[$mode .'_date']))
         $output.= " disabled=\"disabled\"";
      $output.= " />\n";

      return $output;

   } // get_calendar()

   /**
    * output calendar matrix
    */
   public function get_calendar_matrix($year = 0, $month = 0, $day = 0)
   {
      if (!isset($year)) $year = date('Y');
      if (!isset($month)) $month = date('m');
      if (!isset($day)) $day = date('d');
      $rows = 1;
      $cols = 1;
      $matrix = Array();

      require_once CALENDAR_ROOT.'Month/Weekdays.php';
      require_once CALENDAR_ROOT.'Day.php';

      // Build the month
      $month = new Calendar_Month_Weekdays($year,$month);

      // Create links
      $prevStamp = $month->prevMonth(true);
      $prev = "javascript:setMonth(". date('Y',$prevStamp) .", ". date('n',$prevStamp) .", ". date('j',$prevStamp) .");";
      $nextStamp = $month->nextMonth(true);
      $next = "javascript:setMonth(". date('Y',$nextStamp) .", ". date('n',$nextStamp) .", ". date('j',$nextStamp) .");";

      $selectedDays = array (
         new Calendar_Day($year,$month,$day),
         new Calendar_Day($year,12,25),
      );

      // Build the days in the month
      $month->build($selectedDays);

      $this->tmpl->assign('current_month', date('F Y',$month->getTimeStamp()));
      $this->tmpl->assign('prev_month', $prev);
      $this->tmpl->assign('next_month', $next);

      while ( $day = $month->fetch() ) {
   
         if(!isset($matrix[$rows]))
            $matrix[$rows] = Array();

         $string = "";

         $dayStamp = $day->thisDay(true);
         $link = "javascript:setCalendarDate(". date('Y',$dayStamp) .", ". date('n',$dayStamp).", ". date('j',$dayStamp) .");";

         // isFirst() to find start of week
         if ( $day->isFirst() )
            $string.= "<tr>\n";

         if ( $day->isSelected() ) {
            $string.= "<td class=\"selected\">".$day->thisDay()."</td>\n";
         } else if ( $day->isEmpty() ) {
            $string.= "<td>&nbsp;</td>\n";
         } else {
            $string.= "<td><a class=\"calendar\" href=\"".$link."\">".$day->thisDay()."</a></td>\n";
         }

         // isLast() to find end of week
         if ( $day->isLast() )
            $string.= "</tr>\n";

         $matrix[$rows][$cols] = $string;

         $cols++;

         if($cols > 7) {
            $cols = 1;
            $rows++;
         }
      }

      $this->tmpl->assign('matrix', $matrix);
      $this->tmpl->assign('rows', $rows);
      $this->tmpl->show("calendar.tpl");

   } // get_calendar_matrix()

   /**
    * output export page
    */
   public function getExport($mode)
   {
      $pictures = $this->getPhotoSelection();
      $current_tags = $this->getCurrentTags();  

      foreach($pictures as $picture) {

         $orig_url = $this->get_phpfspot_url() ."index.php?mode=showp&id=". $picture;
         if($current_tags != "") {
            $orig_url.= "&tags=". $current_tags;
         } 
         if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
            $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
         }

         $thumb_url = $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;

         switch($mode) {

            case 'HTML':
               // <a href="%pictureurl%"><img src="%thumbnailurl%" ></a>
               print htmlspecialchars("<a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>") ."<br />\n";
               break;
               
            case 'MoinMoin':
               // "[%pictureurl% %thumbnailurl%]"
               print htmlspecialchars("[".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
               break;

            case 'MoinMoinList':
               // " * [%pictureurl% %thumbnailurl%]"
               print "&nbsp;" . htmlspecialchars("* [".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
               break;
         }

      }

   } // getExport()

   /**
    * output RSS feed
    */
   public function getRSSFeed()
   {
      Header("Content-type: text/xml; charset=utf-8");
      print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
?>
<rss version="2.0"
   xmlns:media="http://search.yahoo.com/mrss/"
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 >
 <channel>
  <title>phpfspot</title>
  <description>phpfspot RSS feed</description>
  <link><?php print htmlspecialchars($this->get_phpfspot_url()); ?></link>
  <pubDate><?php print strftime("%a, %d %b %Y %T %z"); ?></pubDate>
  <generator>phpfspot</generator>
<?php

      $pictures = $this->getPhotoSelection();
      $current_tags = $this->getCurrentTags();  

      foreach($pictures as $picture) {

         $orig_url = $this->get_phpfspot_url() ."index.php?mode=showp&id=". $picture;
         if($current_tags != "") {
            $orig_url.= "&tags=". $current_tags;
         } 
         if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
            $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
         }

         $details = $this->get_photo_details($picture);

         $thumb_url = $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
         $thumb_html = htmlspecialchars("
<a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>
<br>
". $details['description']);

         $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
         $meta = $this->get_meta_informations($orig_path);
         $meta_date = isset($meta['FileDateTime']) ? $meta['FileDateTime'] : filemtime($orig_path);

?>
  <item>
   <title><?php print htmlspecialchars($this->parse_uri($details['uri'], 'filename')); ?></title>
   <link><?php print htmlspecialchars($orig_url); ?></link>
   <guid><?php print htmlspecialchars($orig_url); ?></guid>
   <dc:date.Taken><?php print strftime("%Y-%m-%dT%H:%M:%S+00:00", $meta_date); ?></dc:date.Taken>
   <description>
    <?php print $thumb_html; ?> 
   </description>
   <pubDate><?php print strftime("%a, %d %b %Y %T %z", $meta_date); ?></pubDate>
  </item>
<?php

      }
?>
 </channel>
</rss>
<?php


   } // getExport()

 
   /**
    * return all selected tags as one string
    */
   private function getCurrentTags()
   {
      $current_tags = "";
      if(isset($_SESSION['selected_tags']) && $_SESSION['selected_tags'] != "") {
         foreach($_SESSION['selected_tags'] as $tag)
            $current_tags.= $tag .",";
         $current_tags = substr($current_tags, 0, strlen($current_tags)-1);
      }
      return $current_tags;

   } // getCurrentTags()

   /**
    * return the current photo
    */
   public function getCurrentPhoto()
   {
      if(isset($_SESSION['current_photo'])) {
         print $_SESSION['current_photo'];
      }
   } // getCurrentPhoto()

   /**
    * tells the client browser what to do
    *
    * this function is getting called via AJAX by the
    * client browsers. it will tell them what they have
    * to do next. This is necessary for directly jumping
    * into photo index or single photo view when the are
    * requested with specific URLs
    */
   public function whatToDo()
   {
      if(isset($_SESSION['current_photo']) && $_SESSION['start_action'] == 'showp') {
         return "show_photo";
      }
      elseif(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
         return "showpi_tags";
      }
      elseif(isset($_SESSION['start_action']) && $_SESSION['start_action'] == 'showpi') {
         return "showpi";
      }

      return "nothing special";

   } // whatToDo()

   /**
    * return the current process-user
    */
   private function getuid()
   {
      if($uid = posix_getuid()) {
         if($user = posix_getpwuid($uid)) {
            return $user['name'];
         }
      }
   
      return 'n/a';
   
   } // getuid()

   /**
    * returns a select-dropdown box to select photo index sort parameters
    */
   public function smarty_sort_select_list($params, &$smarty)
   {
      $output = "";

      foreach($this->sort_orders as $key => $value) {
         $output.= "<option value=\"". $key ."\"";
         if($key == $_SESSION['sort_order']) {
            $output.= " selected=\"selected\"";
         }
         $output.= ">". $value ."</option>";
      }

      return $output;

   } // smarty_sort_select_list()

   /**
    * returns the currently selected sort order
    */ 
   private function get_sort_order()
   {
      switch($_SESSION['sort_order']) {
         case 'date_asc':
            return " ORDER BY p.time ASC";
            break;
         case 'date_desc':
            return " ORDER BY p.time DESC";
            break;
         case 'name_asc':
            if($this->dbver < 9) {
               return " ORDER BY p.name ASC";
            }
            else {
               return " ORDER BY basename(p.uri) ASC";
            }
            break;
         case 'name_desc':
            if($this->dbver < 9) {
               return " ORDER BY p.name DESC";
            }
            else {
               return " ORDER BY basename(p.uri) DESC";
            }
            break;
         case 'tags_asc':
            return " ORDER BY t.name ASC ,p.time ASC";
            break;
         case 'tags_desc':
            return " ORDER BY t.name DESC ,p.time ASC";
            break;
      }

   } // get_sort_order()

   /***
     * return the next to be shown slide show image
     *
     * this function returns the URL of the next image
     * in the slideshow sequence.
     */
   public function getNextSlideShowImage()
   {
      $all_photos = $this->getPhotoSelection();

      if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == count($all_photos)-1) 
         $_SESSION['slideshow_img'] = 0;
      else
         $_SESSION['slideshow_img']++;

      return $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;

   } // getNextSlideShowImage()

   /***
     * return the previous to be shown slide show image
     *
     * this function returns the URL of the previous image
     * in the slideshow sequence.
     */
   public function getPrevSlideShowImage()
   {
      $all_photos = $this->getPhotoSelection();

      if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == 0)
         $_SESSION['slideshow_img'] = 0;
      else
         $_SESSION['slideshow_img']--;

      return $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;

   } // getPrevSlideShowImage()

   public function resetSlideShow()
   {
      if(isset($_SESSION['slideshow_img']))
         unset($_SESSION['slideshow_img']);

   } // resetSlideShow()
   
   /***
     * get random photo
     *
     * this function will get all photos from the fspot
     * database and randomly return ONE entry
     *
     * saddly there is yet no sqlite3 function which returns
     * the bulk result in array, so we have to fill up our
     * own here.
     */ 
   public function get_random_photo()
   {
      $all = Array();

      $result = $this->db->db_query("
         SELECT id
         FROM photos
      ");
      
      while($row = $this->db->db_fetch_object($result)) {
         array_push($all, $row['id']);
      }

      return $all[array_rand($all)];

   } // get_random_photo()

   /**
    * validates provided date
    *
    * this function validates if the provided date
    * contains a valid date and will return true 
    * if it is.
    */
   public function isValidDate($date_str)
   {
      $timestamp = strtotime($date_str);
   
      if(is_numeric($timestamp))
         return true;
      
      return false;

   } // isValidDate()

   /**
    * timestamp to string conversion
    */
   private function ts2str($timestamp)
   {
      return strftime("%Y-%m-%d", $timestamp);
   } // ts2str()

   private function extractTags($tags_str)
   {
      $not_validated = split(',', $_GET['tags']);
      $validated = array();

      foreach($not_validated as $tag) {
         if(is_numeric($tag))
            array_push($validated, $tag);
      }
   
      return $validated;
   
   } // extractTags()

   /**
    * returns the full path to a thumbnail
    */
   public function get_thumb_path($width, $photo)
   {
      $md5 = $this->getMD5($photo);
      $sub_path = substr($md5, 0, 2);
      return $this->cfg->thumb_path
         . "/"
         . $sub_path
         . "/"
         . $width
         . "_"
         . $md5;

   } // get_thumb_path()

   /**
    * returns server's virtual host name
    */
   private function get_server_name()
   {
      return $_SERVER['SERVER_NAME'];
   } // get_server_name()

   /**
    * returns type of webprotocol which is
    * currently used
    */
   private function get_web_protocol()
   {
      if(!isset($_SERVER['HTTPS']))
         return "http";
      else
         return "https";
   } // get_web_protocol()

   /**
    * return url to this phpfspot installation
    */
   private function get_phpfspot_url()
   {
      return $this->get_web_protocol() ."://". $this->get_server_name() . $this->cfg->web_path;
   } // get_phpfspot_url()
   
   /**
    * check file exists and is readable
    *
    * returns true, if everything is ok, otherwise false
    * if $silent is not set, this function will output and
    * error message
    */
   private function check_readable($file, $silent = null)
   {
      if(!file_exists($file)) {
         if(!isset($silent))
            print "File \"". $file ."\" does not exist.\n";
         return false;
      }

      if(!is_readable($file)) {
         if(!isset($silent))
            print "File \"". $file ."\" is not reachable for user ". $this->getuid() ."\n";
         return false;
      }

      return true;

   } // check_readable()

   /**
    * check if all needed indices are present
    *
    * this function checks, if some needed indices are already
    * present, or if not, create them on the fly. they are
    * necessary to speed up some queries like that one look for
    * all tags, when show_tags is specified in the configuration.
    */
   private function checkDbIndices()
   {
      $result = $this->db->db_exec("
         CREATE INDEX IF NOT EXISTS
            phototag
         ON
            photo_tags
               (photo_id, tag_id)
      ");

   } // checkDbIndices()

   /**
    * retrive F-Spot database version
    *
    * this function will return the F-Spot database version number
    * It is stored within the sqlite3 database in the table meta
    */
   public function getFspotDBVersion()
   {
      if($result = $this->db->db_fetchSingleRow("
         SELECT data as version
         FROM meta
         WHERE
            name LIKE 'F-Spot Database Version'
      "))
         return $result['version'];

      return null;

   } // getFspotDBVersion()

   /**
    * parse the provided URI and will returned the
    * requested chunk
    */
   public function parse_uri($uri, $mode)
   {
      if(($components = parse_url($uri)) !== false) {

         switch($mode) {
            case 'filename':
               return basename($components['path']);
               break;
            case 'dirname':
               return dirname($components['path']);
               break;
            case 'fullpath':
               return $components['path'];
               break;
         }
      }

      return $uri;

   } // parse_uri()

   /**
    * validate config options
    *
    * this function checks if all necessary configuration options are
    * specified and set.
    */
   private function check_config_options()
   {
      if(!isset($this->cfg->page_title) || $this->cfg->page_title == "")
         $this->_error("Please set \$page_title in phpfspot_cfg");

      if(!isset($this->cfg->base_path) || $this->cfg->base_path == "")
         $this->_error("Please set \$base_path in phpfspot_cfg");

      if(!isset($this->cfg->web_path) || $this->cfg->web_path == "")
         $this->_error("Please set \$web_path in phpfspot_cfg");

      if(!isset($this->cfg->thumb_path) || $this->cfg->thumb_path == "")
         $this->_error("Please set \$thumb_path in phpfspot_cfg");

      if(!isset($this->cfg->smarty_path) || $this->cfg->smarty_path == "")
         $this->_error("Please set \$smarty_path in phpfspot_cfg");

      if(!isset($this->cfg->fspot_db) || $this->cfg->fspot_db == "")
         $this->_error("Please set \$fspot_db in phpfspot_cfg");

      if(!isset($this->cfg->db_access) || $this->cfg->db_access == "")
         $this->_error("Please set \$db_access in phpfspot_cfg");

      if(!isset($this->cfg->phpfspot_db) || $this->cfg->phpfspot_db == "")
         $this->_error("Please set \$phpfspot_db in phpfspot_cfg");

      if(!isset($this->cfg->thumb_width) || $this->cfg->thumb_width == "")
         $this->_error("Please set \$thumb_width in phpfspot_cfg");

      if(!isset($this->cfg->thumb_height) || $this->cfg->thumb_height == "")
         $this->_error("Please set \$thumb_height in phpfspot_cfg");

      if(!isset($this->cfg->photo_width) || $this->cfg->photo_width == "")
         $this->_error("Please set \$photo_width in phpfspot_cfg");

      if(!isset($this->cfg->mini_width) || $this->cfg->mini_width == "")
         $this->_error("Please set \$mini_width in phpfspot_cfg");

      if(!isset($this->cfg->thumbs_per_page))
         $this->_error("Please set \$thumbs_per_page in phpfspot_cfg");

      if(!isset($this->cfg->path_replace_from) || $this->cfg->path_replace_from == "")
         $this->_error("Please set \$path_replace_from in phpfspot_cfg");

      if(!isset($this->cfg->path_replace_to) || $this->cfg->path_replace_to == "")
         $this->_error("Please set \$path_replace_to in phpfspot_cfg");

      if(!isset($this->cfg->hide_tags))
         $this->_error("Please set \$hide_tags in phpfspot_cfg");

      if(!isset($this->cfg->theme_name))
         $this->_error("Please set \$theme_name in phpfspot_cfg");

      if(!isset($this->cfg->logging))
         $this->_error("Please set \$logging in phpfspot_cfg");

      if(isset($this->cfg->logging) && $this->cfg->logging == 'logfile') {

         if(!isset($this->cfg->log_file))
            $this->_error("Please set \$log_file because you set logging = log_file in phpfspot_cfg");

         if(!is_writeable($this->cfg->log_file))
            $this->_error("The specified \$log_file ". $log_file ." is not writeable!");

      }

      /* check for pending slash on web_path */
      if(!preg_match("/\/$/", $this->web_path))
         $this->web_path.= "/";

      return $this->runtime_error;

   } // check_config_options()

   /**
    * cleanup phpfspot own database
    *
    * When photos are getting delete from F-Spot, there will remain
    * remain some residues in phpfspot own database. This function
    * will try to wipe them out.
    */
   public function cleanup_phpfspot_db()
   {
      $to_delete = Array();

      $result = $this->cfg_db->db_query("
         SELECT img_idx
         FROM images
         ORDER BY img_idx ASC
      ");

      while($row = $this->cfg_db->db_fetch_object($result)) {
         if(!$this->db->db_fetchSingleRow("
            SELECT id
            FROM photos
            WHERE id='". $row['img_idx'] ."'")) {

            array_push($to_delete, $row['img_idx'], ',');
         }
      }

      print count($to_delete) ." unnecessary objects will be removed from phpfspot's database.\n";

      $this->cfg_db->db_exec("
         DELETE FROM images
         WHERE img_idx IN (". implode($to_delete) .")
      ");

   } // cleanup_phpfspot_db()

} // class PHPFSPOT

?>