/*
 * (c) 2005 Research Group Dr. Fuhrmann, University of Karlsruhe, Germany
 * All rights reserved.
 *
 * Author:
 *         Roman Krenicky
 *
 * $Id: vdrepg_interface.cpp,v 1.17 2005/11/23 17:51:35 kutzner Exp $
 *
 */

/*
  vdrepg_interface.cpp
  created: 1.12.2004
  last change: 9.7.2005
*/
/*! @file
    interface to the EPG data of the (main) vdr
*/

#include "globalheader.h"

#include <string>
#include <stdexcept>
#include <tools.h>
#include "vdrepg_interface.h"
extern "C"
{
  #include <stdlib.h>
}

//! no new channels are added, if their number would exceed @c MAX_NO_OF_CHANNELS
#define MAX_NO_OF_CHANNELS       2000



/*! @brief calculates the starttime of a time block
    @param time  some time in the block looked for
    @param some_starttime  starttime of @em any time block
    @param block_size  size of a time block (in seconds); must not be 0
    @note @a some_starttime and @a block_size define, what's a time block
          at all
*/
time_t vdrepg_interface::block_starttime
   (time_t time, time_t some_starttime, time_t block_size)
{
   time_t diff;
   // diff is the time difference between the starttime of the block
   // (searched for) and the given time

   if (block_size == 0)  // shouldn't be ever needed !
      return time;

   if (some_starttime <= time)
      diff = (time - some_starttime) % block_size;
   else
   {
      diff = (some_starttime - time) % block_size;
      if (diff != 0)
         diff = block_size - diff;
   } // else

   return (time - diff);
} // block_starttime (...)


/*! @brief looks whether the specified time block has incomplete data
    @note this is an internal helper method only, have a look at
          @c block_is_incomplete(), too
    @param event  points to a pointer to the first event inside or after the
                  time block to examine (is part of a list of events sorted
                  by start times) or points to NULL (in which case the block
                  is incomplete); on return @a *event points to the first
                  event after that time block if it is complete or
                  possibly to an event inside that block if it is incomplete
                  (or is NULL if no such event exists)
    @attention the @c cSchedules list must be locked when calling this method
    @param from  start time of the block (times >= @a from) to search,
                 (@a event->StartTime() >= @a from must hold)
    @param block_size  size of a time block (in seconds)
    @param hole_size  minimum time gap which is considered a hole (in seconds)
    @param min_coverage  minimum time coverage the EPG data of the block must
                         have to be considered complete (0 <= @a coverage <= 1)
    @return @c false if the EPG data is complete, @c true otherwise (including
            the case, that an error occured)
*/
bool vdrepg_interface::incomplete_block
   (const cEvent **event, time_t from, time_t block_size, time_t hole_size,
    float min_coverage)
{
   time_t previous_stoptime;  // ending time of previous event
   time_t gap;  // size of potential current gap
   time_t total_gap = 0;  // total gap size of whole block
   time_t to = from + block_size;  // end of current time block ( < to)
   float coverage;  // time coverage of the block's EPG data

   if ( (block_size == 0) || (min_coverage < 0) || (min_coverage > 1) ||
        (event == NULL) || (*event == NULL) || ((*event)->StartTime() < from) )
      return true;

   // set initial stoptime (to check 'left border'):
   previous_stoptime = from;
   if ( (*event != NULL) && ((*event)->Prev() != NULL) )
   {
      previous_stoptime = ((cEvent*)(*event)->Prev())->StartTime()
                          + ((cEvent*)(*event)->Prev())->Duration();
      // we don't look at the real gap here (only 'our part' of it):
      if (previous_stoptime < from)
         previous_stoptime = from;
   } // if ( (*event != NULL) && ((*event)->Prev() != NULL) )
   // traverse the events with a start time between 'from' and 'to':
   for (; (*event != NULL) && ((*event)->StartTime() < to);
        *event=(cEvent*)(*event)->Next())
   {
      // TODO: what to do with overlapping events?
      if (previous_stoptime > (*event)->StartTime())
         previous_stoptime = (*event)->StartTime();
      // calculate potential gap:
      gap = (*event)->StartTime() - previous_stoptime;
      total_gap += gap;
      if (gap >= hole_size)
         return true;

      previous_stoptime = (*event)->StartTime() + (*event)->Duration();
   } // for (; (*event != NULL) && ((*event)->StartTime() < to); ... )
   // check 'right border':
   if (previous_stoptime < to)
   {
      // we don't look at the real gap here (only 'our part' of it):
      gap = to - previous_stoptime;
      total_gap += gap;
      if (gap >= hole_size)
         return true;
   } // if (previous_stoptime < to)

   // calculate coverage:
   coverage = 1  -  (float)total_gap / (float)block_size;
   if (coverage < min_coverage)
      return true;

   // data is complete:
   return false;
} // incomplete_block (...)


/*! @brief chooses a random (non-empty) block in the global EPG database
    @param schedules  the global EPG database
    @attention @a schedules must be locked when calling this method
    @param block_size  size of a time block (in seconds)
    @param block  the block chosen will be stored here on return; initially
                  the starttime entry must be a valid block starttime, for it
                  defines what's considered a block (togethet with
                  @a block_size)
    @param schedulepp  a pointer to the schedule, the chosen block is contained
                       in, will be stored here
    @return a pointer to the first entry in the block chosen or NULL on error
    @note @c rand() is used to get random numbers
*/
const cEvent* vdrepg_interface::pick_random_block
   (const cSchedules *schedules, time_t block_size, blockentry *block,
    const cSchedule **schedulepp)
{
   int no_of_channels, channel_no_chosen;
   int no_of_events, event_no_chosen;
   time_t starttime;
   const cSchedule *schedule, *tmp_schedule;
   const cChannel *vdrchannel;
   epgchannel channel;
   const cList<cEvent> *events = NULL;
   const cEvent *event;


   if ( (schedules == NULL) || (block == NULL) )
      return NULL;

   // randomly choose a channel/schedule:
   no_of_channels = schedules->Count();
   if (no_of_channels == 0)
      return NULL;
      // we obstinately use the least significant bits:
   channel_no_chosen = rand() % no_of_channels;
      // find the corresponding schedule:
   schedule = schedules->Get (channel_no_chosen);

   // we need a non-empty schedule:
   tmp_schedule = schedule;
   while (tmp_schedule != NULL)
   {
      // get the schedule's list of EPG entries ("events"):
      events = tmp_schedule->Events();
      if (events == NULL)
         continue;
      if (events->First() != NULL)
         break;
      tmp_schedule = schedules->Next (tmp_schedule);
   } // while (tmp_schedule != NULL)
   if (tmp_schedule == NULL)
   {
      tmp_schedule = schedules->First();
      while ( (tmp_schedule != NULL) && (tmp_schedule != schedule) )
      {
         // get the schedule's list of EPG entries ("events"):
         events = tmp_schedule->Events();
         if (events == NULL)
            continue;
         if (events->First() != NULL)
            break;
         tmp_schedule = schedules->Next (tmp_schedule);
      } // while ( (tmp_schedule != NULL) && (tmp_schedule != schedule) )
      if ( (tmp_schedule == NULL) ||
           (events == NULL) || (events->First() == NULL) )
         return NULL;  // there's no event entry at all !
   } // if (tmp_schedule == NULL)
   schedule = tmp_schedule;


   // get the channel, this schedule belongs to:
   vdrchannel = Channels.GetByChannelID(schedule->ChannelID(), true);
   if (vdrchannel == NULL)
      return NULL;
   if ( !vdrchannel2epgchannel(*vdrchannel, &channel) )
      return NULL;


   // get the schedule's list of EPG entries ("events"):
   events = schedule->Events();
   if (events == NULL)
      return NULL;

   // randomly choose an event:
   no_of_events = events->Count();
   if (no_of_events == 0)
      return NULL;
      // we obstinately use the least significant bits:
   event_no_chosen = rand() % no_of_events;
      // find the corresponding event:
   event = events->Get (event_no_chosen);


   // calculate the starttime of the block, the chosen event belongs to:
   starttime = block_starttime (event->StartTime(),
                                block->get_start_time(), block_size);


   // find the first event in the block:
   while ( (events->Prev(event) != NULL) &&
           (events->Prev(event)->StartTime() >= starttime) )
      event=events->Prev(event);


   // return values:
   *block = blockentry(channel, starttime);
   *schedulepp = schedule;
   return event;
} // pick_random_block (...)


/*! @brief writes EPG data to an 'epgdata' structure
    @param data  the EPG data requested will be stored here; @a '*data' will
                 @em not be emptied first, the new data is appended
    @param event  the first event of the EPG data
    @attention the @c cSchedules list must be locked when calling this method
    @param to  gather EPG data, while the events have a @em starttime < @a to
*/
void vdrepg_interface::dump_epgdata
   (epgdata *data, const cEvent *event, time_t to)
{
   if ( (data == NULL) || (event == NULL) )
      return;

   // we do it the way, cSchedule::Dump() does it:


   // collect the events with a start time between 'from' and 'to':
   for (; (event != NULL) && (event->StartTime() < to);
        event=(const cEvent*)event->Next())
   {
      data->data << "E " << event->EventID() << ' '
                   << event->StartTime() << ' '
                   << event->Duration() << ' '
                   << hex << (unsigned)event->TableID() << ' '
                          << (unsigned)event->Version() << dec << '\n';
      if ( !isempty(event->Title()) )
         data->data << "T " << event->Title() << '\n';
      if ( !isempty(event->ShortText()) )
         data->data << "S " << event->ShortText() << '\n';
      if ( !isempty(event->Description()) )
      {
         const char *description = event->Description();
         data->data << "D ";
         for (unsigned int i=0; description[i]; i++)
            if (description[i] == '\n')
               data->data << '|';
            else
               data->data << description[i];
         data->data << '\n';
      } // if ( !isempty(event->Description()) )
      if (event->Vps())
         data->data << "V " << event->Vps() << '\n';
      data->data << "e\n";
   } // for (; (event != NULL) && (event->StartTime() < to); ...)
} // dump_epgdata (...)


/*! just a constructor
*/
vdrepg_interface::vdrepg_interface (void)
{
} // vdrepg_interface()


/*! just a destructor
*/
vdrepg_interface::~vdrepg_interface (void)
{
} // ~vdrepg_interface()


/*! @brief fetches EPG data from the global vdr EPG table
    @param channel  the channel to search
    @param from  the EPG events have a @em starttime >= @a from
    @param to  the EPG events have a @em starttime < @a to
    @param complete_only  returns data only, if the requested block is
                          complete (-> @a hole_size, @a min_coverage)
                          [default is true]
    @param hole_size  minimum time gap which is considered a hole (in seconds)
                      [default is 0]
    @param min_coverage  minimum time coverage the EPG data of the block must
                         have to be considered complete (0 <= @a coverage <= 1)
                         [default is 1]
    @warning @a to doesn't refer to the ending time of the EPG event(s)
    @return the requested data or NULL on error, where the data may be empty;
            the data is a dynamically allocated object, don't forget to delete
            it!
*/
epgdata* vdrepg_interface::get_epg
   (const epgchannel &channel, time_t from, time_t to, bool complete_only,
    time_t hole_size, float min_coverage)
{
   if (to <= from)
      return NULL;

   epgdata *retval = NULL;

   const cSchedules *schedules;
   const cSchedule *schedule;
   const cChannel *vdrchannel;
   const cList<cEvent> *events;
   const cEvent *event;
   cChannel tmpchannel;

   // open and lock the global list of schedules:
   { // name scope of 'schedules_lock'
   cSchedulesLock schedules_lock;
   if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   {
      // find the requested channel in the channels list:
      if ( !epgchannel2vdrchannel(channel, &tmpchannel) )
         return NULL;
         // NOTE: we don't want 'tmpchannel' but the channel with the same ID
         //       in our local channel list:
      vdrchannel = Channels.GetByChannelID (tmpchannel.GetChannelID(), true);
      if (vdrchannel == NULL)  // this is no error, we simply don't have the
         return new epgdata;   // data (not knowing the channel)

      // each schedule is the EPG list for one channel (fetch the right one):
      schedule = schedules->GetSchedule (vdrchannel->GetChannelID());
      if (schedule == NULL)
         return new epgdata;   // no EPG data for this channel

      // get the schedule's list of EPG entries ("events"):
      events = schedule->Events();
      if (events == NULL)
         return NULL;

      // a schedule is sorted by start times

      // skip events starting before 'from':
      for ( event=events->First();
            (event != NULL) && (event->StartTime() < from);
            event=events->Next(event) );

      // check for completeness when complete block is requested:
      if (complete_only)
      {
         const cEvent *event_copy = event;
         if ( incomplete_block(&event_copy, from, (to-from),
                               hole_size, min_coverage) )
            return NULL;
      } // if (complete_only)


      // OK, so let's gather the data:
      retval = new epgdata;
      dump_epgdata (retval, event, to);
   } // if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   else
      return NULL;
   } // name scope of 'schedules_lock'

   return retval;
} // get_epg (...)


/*! @brief puts EPG data into the global vdr EPG table if it is more up to
           date than what is already stored
    @note if the @a channel does not exist yet, it is inserted, too
    @param channel  the channel, the EPG data belongs to
    @param data  the data
    @param isnew  if not NULL, will be @c true if the @a data has been some new
           information, @c false otherwise (on successful return only;
           default is NULL)
    @return @c true on success, @c false if an error occured
*/
bool vdrepg_interface::put_epg
   (const epgchannel &channel, const epgdata &data, bool *isnew)
{
   string epg_string = data.data.str();
   string::size_type length = epg_string.size();
   string tmp_string;
   // position of the next character to read (in epg_string):
   string::size_type pos = 0;
   string::size_type endpos = 0;
   char *endptr;  // for strtoul()
   bool updated_data = false;  // will be stored in *isnew
   bool skip_event;
   cSchedules *schedules = NULL;
   cSchedule *schedule = NULL;
   cEvent *event;
   cChannel tmpchannel;
   tChannelID channelID;
   unsigned int eventID;
   time_t startTime;
   int duration;
   unsigned int tableID;
   unsigned int version;


   // calculate the channelID of the requested channel:
   if ( !epgchannel2vdrchannel(channel, &tmpchannel) )
      return false;
   channelID = tmpchannel.GetChannelID();
   if ( !channelID.Valid() )
      return false;


   // is the requested channel in the channel list? (if not, add it):
      // NOTE: identify by channel id:
   if ( Channels.GetByChannelID(channelID, true) == NULL )
   {  // add channel
      // prevent ramming up the channel list:
      if (Channels.Count() >= MAX_NO_OF_CHANNELS)
         return false;

      cChannel *newchannel;
      if ( !Channels.Lock(/*write*/true) )
         return false;
      newchannel = Channels.NewChannel (&tmpchannel, tmpchannel.Name(),
                              tmpchannel.ShortName(), tmpchannel.Provider(),
                              tmpchannel.Nid(), tmpchannel.Tid(),
                              tmpchannel.Sid(), tmpchannel.Rid());
      Channels.SetModified();
      Channels.Unlock();
      if (newchannel == NULL)
         return false;
   } // if ( Channels.GetByChannelID(channelID, true) == NULL )


   // open and lock the global list of schedules:
   { // name scope of 'schedules_lock'
   // TODO: timeout for write lock?
   cSchedulesLock schedules_lock(true); // write lock
   if ((schedules=(cSchedules*)cSchedules::Schedules(schedules_lock)) != NULL)
   {
      // we access the string via string::at() instead of string::operator[]
      // so we don't have to look for validity of the position explicitly,
      // we just catch the 'out_of_range' exception (child of 'exception')
      try
      {
         // get schedule corresponding to the channel ID:
         schedule = schedules->AddSchedule (channelID);
         if (schedule == NULL)
            return false;

         // parse the EPG data:
         while ( (pos != string::npos) && (length-pos != 0) )
         {
            // traverse schedule data (events):
            event = NULL;
               // skip newlines and spaces:
            pos = epg_string.find_first_not_of (" \n", pos);
            // don't return on error in here, throw exception() instead, so the
            // cleanup in the catch directive is performed:
            do
            {
               switch (epg_string.at(pos++))
               {
                  case 'E':
                     if (event != NULL)
                        throw exception();

                     skip_event = false;
                     // event ID:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of 'word':
                     endpos = epg_string.find (' ', pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // TODO: mind the conversion! (long int to int)
                     eventID = (unsigned int)strtoul (tmp_string.c_str(),
                                                      &endptr, 10);
                     if (*endptr != (char)0)  throw exception();

                     // start time:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of 'word':
                     endpos = epg_string.find (' ', pos);
                     if (endpos == string::npos)     throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // TODO: mind the conversion!
                     startTime = (time_t)strtoul (tmp_string.c_str(),
                                                  &endptr, 10);
                     if (*endptr != (char)0)  throw exception();

                     // duration:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of 'word':
                     endpos = epg_string.find (' ', pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // TODO: mind the conversion! (long int to int)
                     duration = (int)strtol (tmp_string.c_str(), &endptr, 10);
                     if (*endptr != (char)0)  throw exception();

                     // table ID (mandatory!):
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of 'word':
                     endpos = epg_string.find (' ', pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // TODO: mind the conversion! (long int to int)
                        // this is a hexadecimal number:
                     tableID = (unsigned int)strtoul (tmp_string.c_str(),
                                                      &endptr, 16);
                     if (*endptr != (char)0)  throw exception();

                     // version (mandatory!):
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of 'word' (last one in line):
                     endpos = epg_string.find_first_of (" \n", pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // TODO: mind the conversion! (long int to int)
                        // this is a hexadecimal number:
                     version = (unsigned int)strtoul (tmp_string.c_str(),
                                                      &endptr, 16);
                     if (*endptr != (char)0)  throw exception();


                     // this should be the end of line:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                     if (epg_string.at(pos) != '\n')
                        throw exception();


                     // edit/create event:
                     event = (cEvent *)schedule->GetEvent (eventID, startTime);
                        // don't insert events defined manually by the user:
                     if (tableID == 0)
                        skip_event = true;
                        // look whether the received data is more up to date:
                     else if (event != NULL)
                     {
                        // don't change events defined manually by the user:
                        // (no matter, whether we insert such events or not)
                        if (event->TableID() == 0)
                           skip_event = true;
                        // the lower the table ID, the more current the data:
                        else if (event->TableID() < tableID)
                           skip_event = true;
                        // the higher the version number inside ONE table, the
                        // more curent the data:
                        else if ( (event->TableID() == tableID) &&
                                  (event->Version() >= version) )
                           skip_event = true;
                     } // else if (event != NULL)


                     if (skip_event)
                     {
                           // search end of event:
                        endpos = epg_string.find ("\ne", pos);
                        if (endpos == string::npos)  throw exception();
                        pos = endpos + 2;  // TODO: what about overflows?
                        event = NULL;
                     } // if (skip_event)
                     else // !skip_event
                     {
                        bool new_event = false;
                        if (event == NULL)
                        {
                           event = new cEvent(eventID);
                           new_event = true;
                        } // if (event == NULL)
                        event->SetTableID(tableID);
                        event->SetVersion(version);
                           // has anything changed? :
                        if ( (event->StartTime() != startTime) ||
                             (event->Duration() != duration) )
                           updated_data = true;
                        event->SetStartTime(startTime);
                        event->SetDuration(duration);
                        if (new_event)
                           event = schedule->AddEvent (event);
                        if (event == NULL)
                           throw exception();
                     } // else -> !skip_event

                     break;

                  case 'T':
                     if (event == NULL)
                        throw exception();
                     // title:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of line:
                     endpos = epg_string.find ('\n', pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;

                        // has anything changed? :
                     if ( (event->Title() == NULL) ||
                          (tmp_string.compare(event->Title()) != 0) )
                     {
                        updated_data = true;
                        event->SetTitle (tmp_string.c_str());
                     } // if ( (event->Title() == NULL) || ... )
                     break;

                  case 'S':
                     if (event == NULL)
                        throw exception();
                     // short text:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of line:
                     endpos = epg_string.find ('\n', pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;

                        // has anything changed? :
                     if ( (event->ShortText() == NULL) ||
                          (tmp_string.compare(event->ShortText()) != 0) )
                     {
                        updated_data = true;
                        event->SetShortText (tmp_string.c_str());
                     } // if ( (event->ShortText() == NULL) || ... )
                     break;

                  case 'D':
                     if (event == NULL)
                        throw exception();
                     // description:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of line:
                     endpos = epg_string.find ('\n', pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // replace '|' with '\n':
                     { // name-scope of tmpstr_len
                     string::size_type tmpstr_len = tmp_string.size();
                     for (string::size_type i=0; i < tmpstr_len; i++)
                        if (tmp_string[i] == '|')
                           tmp_string[i] = '\n';
                     } // name-scope of tmpstr_len

                        // has anything changed? :
                     if ( (event->Description() == NULL) ||
                          (tmp_string.compare(event->Description()) != 0) )
                     {
                        updated_data = true;
                        event->SetDescription (tmp_string.c_str());
                     } // if ( (event->Description() == NULL) || ... )
                     break;

                  case 'V':
                     if (event == NULL)
                        throw exception();

                     time_t vps;

                     // vps:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                        // search end of 'word' (last one in line):
                     endpos = epg_string.find_first_of (" \n", pos);
                     if (endpos == string::npos)  throw exception();
                        // copy substring:
                     tmp_string = epg_string.substr (pos, endpos-pos);
                     pos = endpos;
                        // TODO: mind the conversion!
                     vps = (time_t)strtoul (tmp_string.c_str(), &endptr, 10);
                     if (*endptr != (char)0)  throw exception();
                     // this should be the end of line:
                        // skip spaces:
                     pos = epg_string.find_first_not_of (' ', pos);
                     if (epg_string.at(pos) != '\n')
                        throw exception();

                        // has anything changed? :
                     if (event->Vps() != vps)
                     {
                        updated_data = true;
                        event->SetVps (vps);
                     } // if (event->Vps() != vps)
                     break;

                  case 'e':
                     if (event == NULL)
                        throw exception();
                     event = NULL;
                     break;

                  default:
                     // generously ignore unknown enries INSIDE an event
                     // definition:
                     if (event == NULL)
                        throw exception();
                        // search end of line:
                     endpos = epg_string.find ('\n', pos);
                     if (endpos == string::npos)  throw exception();
                     pos = endpos;
               } // switch (epg_string.at(pos++))

               // skip newlines and spaces:
               pos = epg_string.find_first_not_of (" \n", pos);
            } while (pos != string::npos);

            if (event != NULL)
               throw exception();

            schedule->Sort();
            schedules->SetModified(schedule);

            // are we finished already?
            if (pos == string::npos)
               break;
            pos++;

            // skip newlines and spaces:
            pos = epg_string.find_first_not_of (" \n", pos);
            if (pos == string::npos)
               break;
         } // while ( (pos != string::npos) && (length-pos != 0) )
      } // try
      catch (std::exception&)
      {
         if (schedule != NULL)
         {
            schedule->Sort();
            schedules->SetModified(schedule);
         } // if (schedule != NULL)
         /* TODO: should we do a cSchedules::Cleanup(true); ?
                  yet, we mustn't place it inside the scope of the write lock!
         */
         return false;
      } // catch (std::exception&)
   } // if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   else
      return false;
   } // name scope of 'schedules_lock'

   // the Cleanup() must be outside of the write lock scope (!!!):
   cSchedules::Cleanup(true);
   if (isnew != NULL)
      *isnew = updated_data;
   return true;
} // put_epg (...)


/*! @brief fetches blocks of EPG data from the global vdr EPG table in a
           certain order
    @note a kind of 'cyclic order' is used, so if the last block has already
          been selected, the next one returned is the first block again
    @note don't care about the order otherwise
    @param last  the last block returned (or rather the next block returned
                 will be the next complete one after @a last; in particular the
                 @a last block doesn't have to exist at all); if @a last can't
                 be located (in particular if it has an empty channel entry)
                 a block is picked randomly using @c rand() (so you might
                 choose to set a seed with @c srand() before)
    @warning the starttime entry of @a last must be a valid block starttime
             in @em any case, for it defines what's considered a block
             (togethet with @a block_size)
    @param block_size  size of a time block (in seconds)
    @param next  if @a next is not NULL, *next will represent the block
                 returned (if any) and won't be changed if NULL is returned
    @note @a last and @a next may refer to the same object
    @param hole_size  minimum time gap which is considered a hole (in seconds)
    @param min_coverage  minimum time coverage the EPG data of the block must
                         have to be considered complete (0 <= @a coverage <= 1)
    @return the EPG data of the next complete block after @a last if such one
            has been found or NULL otherwise; the data is a dynamically
            allocated object, don't forget to delete it!
*/
epgdata* vdrepg_interface::get_next_complete_block
   (const blockentry &last, time_t block_size, blockentry *next,
    time_t hole_size, float min_coverage)
{
   epgdata *retval = NULL;
   const cSchedules *schedules;
   const cSchedule *schedule;
   const cSchedule *schedule_started_with;
   cChannel tmpchannel, *vdrchannel;
   const cList<cEvent> *events;
   const cEvent *event, *tmp_event;
   epgchannel channel;
   time_t starttime;
   bool found_last_block;


   if ( (block_size == 0) || (min_coverage < 0) || (min_coverage > 1) )
      return NULL;


   // open and lock the global list of schedules:
   { // name scope of 'schedules_lock'
   cSchedulesLock schedules_lock;
   if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   {
      found_last_block = false;
      // find the first event of the next block after the last block returned
      // (in other words: a candidate to look at):
      do
      {
         time_t next_block_starttime;

         // find schedule of the last block returned (identify by channel id):
         if ( !epgchannel2vdrchannel(last.get_channel(), &tmpchannel) )
            break;
         schedule = schedules->GetSchedule (tmpchannel.GetChannelID());
         if (schedule == NULL)
            break;
         // get the schedule's list of EPG entries ("events"):
         events = schedule->Events();
         if (events == NULL)
            break;
         // go to the next block:
         next_block_starttime = last.get_start_time() + block_size;
         // find first event in or after the last block returned:
         for (event=events->First();
              (event != NULL) && (event->StartTime() < next_block_starttime);
              event=events->Next(event) );

         // 'event' might be NULL here, but at least 'schedule' must be correct
         found_last_block = true;
      } while (false);

      // pick a random block, if the last one has not been found:
      if (!found_last_block)
      {
         blockentry block(last);

         event = pick_random_block (schedules, block_size, &block, &schedule);
         if (event == NULL)
            return NULL;
      } // if (!found_last_block)


      /* now 'schedule' must point to some schedule and 'event' must either
         point to an event in '*schedule' or be NULL */

      // get the schedule's list of EPG entries ("events"):
      events = schedule->Events();
      if (events == NULL)
         return NULL;
      // set initial starttime:
      if (event != NULL)
         starttime = block_starttime (event->StartTime(),
                                      last.get_start_time(), block_size);
      else
         starttime = 0;


      // search a complete block (starting with 'event')
      // 'starttime' is the starttime of the current block, we look at
      // 'schedule' is the schedule, the current block belongs to
      schedule_started_with = NULL;
      while (true)
      {
         // go to next schedule, if we reached the end:
         if (event == NULL)
         {
            if (schedule == schedule_started_with)
               return NULL;  // no complete block found !
            else if (schedule_started_with == NULL)
               schedule_started_with = schedule;
            schedule = schedules->Next(schedule);
            if (schedule == NULL)
               schedule = schedules->First();
            if (schedule == NULL)
               return NULL;
            // get the schedule's list of EPG entries ("events"):
            events = schedule->Events();
            if (events == NULL)
               return NULL;
            event = events->First();
            if (event == NULL)
               continue;
            // set new starttime:
            starttime = block_starttime (event->StartTime(),
                                         last.get_start_time(), block_size);
         } // if (event == NULL)

         // is the current block complete? :
         tmp_event = event;
         if ( !incomplete_block(&tmp_event, starttime, block_size, hole_size,
                                min_coverage) )
            break;

         event = tmp_event;
         // block is incomplete, go to next one in current schedule:
         starttime += block_size;
         // skip the remaining events in the old time block:
         for (; (event != NULL) && (event->StartTime() < starttime);
              event=events->Next(event));
         // set new starttime (maybe there are some blocks missing):
         if (event != NULL)
            starttime = block_starttime (event->StartTime(),
                                         last.get_start_time(), block_size);
      } // while (true)


      // get the channel, the block belongs to:
      vdrchannel = Channels.GetByChannelID(schedule->ChannelID(), true);
      if (vdrchannel == NULL)
         return NULL;
      if ( !vdrchannel2epgchannel(*vdrchannel, &channel) )
         return NULL;


      // gather the data of the block found:
      retval = new epgdata;
      dump_epgdata (retval, event, starttime + block_size);

      // return info about the block found:
      if (next != NULL)
         *next = blockentry(channel, starttime);
   } // if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   else
      return NULL;
   } // name scope of 'schedules_lock'

   return retval;
} // get_next_complete_block (...)


/*! @brief looks whether the specified time block has incomplete data
    @param channel  the channel to search
    @param from  start time of the block to search ( >= @a from)
    @param block_size  size of a time block (in seconds)
    @param hole_size  minimum time gap which is considered a hole (in seconds)
    @param min_coverage  minimum time coverage the EPG data of the block must
                         have to be considered complete (0 <= @a coverage <= 1)
    @return @c false if the EPG data is complete, @c true otherwise (including
            the case, that the @a channel has not been found at all or the
            arguments passed are invalid in some other way)
*/
bool vdrepg_interface::block_is_incomplete
   (const epgchannel &channel, time_t from, time_t block_size,
    time_t hole_size, float min_coverage)
{
   if ( (block_size == 0) || (min_coverage < 0) || (min_coverage > 1) )
      return true;

   const cSchedules *schedules;
   const cSchedule *schedule;
   const cChannel *vdrchannel;
   const cList<cEvent> *events;
   const cEvent *event;
   cChannel tmpchannel;

   // open and lock the global list of schedules:
   { // name scope of 'schedules_lock'
   cSchedulesLock schedules_lock;
   if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   {
      // find the requested channel in the channels list:
      if ( !epgchannel2vdrchannel(channel, &tmpchannel) )
         return true;
         // NOTE: we don't want 'tmpchannel' but the channel with the same ID
         //       in our local channel list:
      vdrchannel = Channels.GetByChannelID (tmpchannel.GetChannelID(), true);
      if (vdrchannel == NULL)
         return true;

      // each schedule is the EPG list for one channel (fetch the right one):
      schedule = schedules->GetSchedule (vdrchannel->GetChannelID());
      if (schedule == NULL)
         return true;

      // get the schedule's list of EPG entries ("events"):
      events = schedule->Events();
      if (events == NULL)
         return true;


      // a schedule is sorted by start times


      // skip events starting before 'from':
      for ( event=events->First();
            (event != NULL) && (event->StartTime() < from);
            event=events->Next(event) );

      // check for completeness:
      return incomplete_block(&event,from,block_size,hole_size,min_coverage);
   } // if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   else
      return true;
   } // name scope of 'schedules_lock'

   return true;
} // block_is_incomplete (...)


/*! @brief finds EPG blocks (time-blocks per channel) with incomlpete data
    @param from  start of time area to search ( >= @a from)
    @param block_size  size of a time block (in seconds)
    @param block_count  number of continuous timeblocks to examine per channel
    @param hole_size  minimum time gap which is considered a hole (in seconds)
    @param min_coverage  minimum time coverage the EPG data of a block must
                         have to be considered complete (0 <= @a coverage <= 1)
    @return a dynamically allocated list of all incomplete blocks in the
            requested range (which may be empty), or NULL on error;
            no entry in the blocklist contains a NULL as channel attribute
    @note the list may be incomplete if minor errors occurred which got ignored
    @warning don't forget to delete the returned list
*/
list<blockentry>* vdrepg_interface::get_incomplete_blocks
   (time_t from, time_t block_size, unsigned int block_count,
    time_t hole_size, float min_coverage)
{
   list<blockentry> *retval = NULL;
   time_t block_start;  // beginning of current time block ( >= block_start)
   time_t block_end;  // end of current time block ( < block_end)
   unsigned int block_no;  // count the blocks, already traversed (per channel)
   bool has_hole;  // the current time block has a hole

   if ( (block_size == 0) || (block_count == 0) ||
        (min_coverage < 0) || (min_coverage > 1) )
      return NULL;

   const cSchedules *schedules;
   const cSchedule *schedule;
   const cChannel *vdrchannel;
   const cList<cEvent> *events;
   const cEvent *event;

   // open and lock the global list of schedules:
   { // name scope of 'schedules_lock'
   cSchedulesLock schedules_lock;
   if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   {
      retval = new list<blockentry>;

      // each schedule is the EPG list for one channel:
      for (schedule=schedules->First(); schedule != NULL;
           schedule=schedules->Next(schedule))
      {
         // get the channel, this schedule belongs to:
         vdrchannel = Channels.GetByChannelID(schedule->ChannelID(), true);
         if (vdrchannel == NULL)
            continue;  // we can't process this schedule

         // get the schedule's list of EPG entries ("events"):
         events = schedule->Events();
         if (events == NULL)
            continue;


         // a schedule is sorted by start times


         // skip events starting before 'from':
         for ( event=events->First();
               (event != NULL) && (event->StartTime() < from);
               event=events->Next(event) );

         // traverse the requested blocks in the current schedule:
         for (block_no=0; block_no < block_count; block_no++)
         {
            // "event" must point to the first event in the current time block
            // if there is such one, otherwise it must point to an event after
            // the current time block or be NULL


            has_hole = false;
            // the bounds of the current time block:
            block_start = from  +  block_no * block_size;
            block_end = block_start + block_size;

            // current block starts at 'block_start':
            if ( incomplete_block(&event, block_start, block_size,
                                  hole_size, min_coverage) )
            {
               epgchannel channel;
                  // if we fail here, we'll miss out some information:
               if ( vdrchannel2epgchannel(*vdrchannel, &channel) )
                  retval->push_back ( blockentry(channel, block_start) );
               // go to the end of the current time block:
               for (; (event != NULL) && (event->StartTime() < block_end);
                    event=events->Next(event));
            } // if ( incomplete_block(&event, block_start, block_size, ...) )
         } // for (block_no=0; block_no < block_count; block_no++)
      } // for (schedule=schedules->First(); schedule != NULL; ... )
   } // if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   else
      return NULL;
   } // name scope of 'schedules_lock'

   return retval;
} // get_incomplete_blocks (...)


/*! @brief checks whether a given channel is known to vdr
    @param channel  the channel to check for
    @return @c true if the channel is known, @c false otherwise or if an error
            occured
*/
bool vdrepg_interface::channel_is_present (const epgchannel &channel)
{
   cChannel tmpchannel;
   tChannelID channelID;


   // calculate the channelID of the requested channel:
   if ( !epgchannel2vdrchannel(channel, &tmpchannel) )
      return false;
   channelID = tmpchannel.GetChannelID();
   if ( !channelID.Valid() )
      return false;

   // is the requested channel in the channel list(?):
      // NOTE: identify by channel id:
   return ( Channels.GetByChannelID(channelID, true) != NULL );
} // channel_is_present (...)


/*! @brief returns a list of all 'EPG-channels'
    @return a dynamically allocated list (which may be empty) of all channels
            we have or had EPG data for, or NULL on error;
    @note the list may be incomplete if minor errors occurred which got ignored
    @warning don't forget to delete the returned list
*/
list<epgchannel>* vdrepg_interface::get_channels (void)
{
   list<epgchannel> *retval = NULL;

   const cSchedules *schedules;
   const cSchedule *schedule;
   const cChannel *vdrchannel;
   epgchannel channel;

   // open and lock the global list of schedules:
   { // name scope of 'schedules_lock'
   cSchedulesLock schedules_lock;
   if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   {
      retval = new list<epgchannel>;

      // each schedule is the EPG list for one channel:
      for (schedule=schedules->First(); schedule != NULL;
           schedule=schedules->Next(schedule))
      {
         // get the channel, this schedule belongs to:
         vdrchannel = Channels.GetByChannelID(schedule->ChannelID(), true);
         if (vdrchannel == NULL)
            continue;  // somewhat fatal, but ignore

         // enter channel to list:
         if ( vdrchannel2epgchannel(*vdrchannel, &channel) )
            retval->push_back (channel);
      } // for (schedule=schedules->First(); schedule != NULL; ... )
   } // if ( (schedules = cSchedules::Schedules(schedules_lock)) != NULL )
   else
      return NULL;
   } // name scope of 'schedules_lock'

   return retval;
} // get_channels()
