/*
 * epg2timers.cxx: Convert an EPG "merkliste" HTML page (http://tvtv.de)
 *                 to timers.conf format for Klaus Schmidinger's vdr
 *                 (http://www.cadsoft.de/people/kls/vdr).
 *
 * Copyright (C) 2000, 2001, 2002, 2003 Carsten Koch
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * Or, point your browser to http://www.gnu.org/copyleft/gpl.html
 *
 * The author can be reached at Carsten.Koch@icem.de
 */


#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

// User-configurable options.

// The following tables maps title and genre names into prefixes
// for the vdr recording name.
// The configuration below is just an example.
// Put in your own choices.
// The string match is a case-sensitive substring match.

typedef struct
{
   const char * string_to_be_matched;
   const char * recording_prefix;
} name_map_entry;

typedef struct
{
   const char * source;
   int sid;
   int nid;
   int tid;
} vdr_channel_entry;


// The title_map maps titles into prefixes for the vdr recording name.
// It has precedence over the genre_map. In other words, if a title
// matches a title map entry, the genre_map is not searched.
static const name_map_entry title_map[] =
{
   {"Outer Limits", "SciFi~"},  // everything that has "Outer Limits" in the title, goes into folder "SciFi"
   {"Andromeda", "SciFi~"},
   {"Farscape", "SciFi~"}
};

// The genre_map maps genres into prefixes for the vdr recording name.
static const name_map_entry genre_map[] =
{
   {"Action", "Action~"},  // everything that has "Action" in the genre, goes into folder "Action"
   {"action", "Action~"},
         
   {"omödie", "Comedy~"},
   {"omedy", "Comedy~"},
   {"Sitcom", "Comedy~"},
         
   {"antasy", "Fantasy~"},
   {"ärchen", "Fantasy~"},

   {"orror", "Horror~"},
   {"rusel", "Horror~"},

   {"inder", "Kids~"},
   {"eichentrick", "Kids~"},

   {"iction", "SciFi~"},

   {"mwelt", "Science~"},
   {"issenschaft", "Science~"},
   {"orschung", "Science~"},
   {"echnik", "Science~"},
   {"eportage", "Science~"},

   {"hriller", "Thriller~"}
};

static const int start_time_safety_margin = 2;  // subtract 2 minutes from start time in case start was early
static const int stop_time_safety_margin = 10;  // add 10 minutes to stop time in case start was delayed

// Do not set max_time_processed to more than 28 days, 
// as VDR cannot handle timer entries that point further than a month into the future.
static const time_t max_time_processed = 21 * 24 * 60 * 60; // process up to 21 days into the future


static const char * const episode = "~EPISODE"; // Use this value if you want vdr episode names to be appended
//static const char * const episode = "";       // Use this value if you do not want vdr episode names to be appended




// Usually, you should not want to change any of these.

static const int max_title   = 256;             // maximum length+1 of title file name generated
static const int max_genre   = 32;              // maximum length+1 of genre text parsed
static const int max_line    = 1024;            // line buffer (not used when parsing summary text)
static const int max_summary = 9000;            // Summary can be up to 9000 bytes long (a bit shorter than vdr's SVDRP command buffer)
static const int max_vdr_channel = 1000;        // maximum size+1 of your channels.conf

static const char * const month_names[] = 
#ifdef english
   {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ""};
#else
   {"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember", ""};
#endif

// The following table maps http://tvtv.de channel names into Astra 19.2E PIDs.
// It is incomplete. Contributions welcome.

typedef struct
{
   const char * tvtv_name;
   unsigned short sid;
} channel_map_entry;


static const channel_map_entry channel_map[] =
{
   {"street", 42},
   {"3sat", 28007},
   {"arte", 28109},
   {"sfb1", 28206},
   {"br", 28107},
   {"alpha", 28112},
   {"ard", 28106},
   {"discovery", 14},
   {"disney", 34},
   {"dsf", 900},
   {"eins_extra", 28201},
   {"festival", 28202},
   {"muxx", 28203},
   {"fox_kids", 28},
   {"hr", 28108},
   {"junior", 19},
   {"kabel", 899},
   {"kika", 28008},
   {"krimi_co", 23},
   {"ktoon", 12},
   {"mdr", 28204},
   {"mtv", 28653},
   {"mtv2", 28640},
   {"n24",47},
   {"ndr", 28224},
   {"neun_live", 897},
   {"orb", 28205},
   {"orf1", 13001},
   {"orf2", 13002},
   {"phoenix", 28114},
   {"planet", 13},
   {"premiere_1", 10},
   {"premiere_2", 11},
   {"premiere_3", 43},
   {"premiere_4", 9},
   {"premiere_5", 29},
   {"premiere_6", 41},
   {"premiere_7", 20},
   {"prem_nostalgie", 516},
   {"prem_scifi", 36},
   {"prem_serie", 16},
   {"premiere_start", 8},
   {"pro7", 898},  
   {"rbb_berlin", 28207},
   {"rbb_brandenburg",28205},
   {"rtl", 12003},
   {"rtl2", 12020},
   {"sat1", 46},
   {"swr_bw", 28113},
   {"swr_rp", 28231},
   {"swr_sr", 28110},
   {"superrtl", 12040},
   {"tele-5", 51},
   {"tw1", 13013},
   {"vox", 12060},
   {"viva", 12732},
   {"viva_plus", 12120},
   {"wdr", 28111},
   {"zdf", 28006},
   {"zdf_doku", 28014},
   {"zdf_info", 28011},  
   {"zdf_theater", 28016},   
//Entries for UK DVB-t
   {"uk_bbc1", 4164},
   {"uk_bbc2", 4228},
   {"uk_bbc_choice", 4351},
   {"uk_bbc4", 16832},
   {"uk_lwt", 8261},
   {"uk_itv2", 8325},
   {"uk_uk_history", 22272},
   {"uk_qvc", 13120},
   {"uk_tvtravelshop", 13632},
   {"uk_bbc_news24", 4415},
   {"uk_sky_news", 22080},
   {"uk_itvnews", 13760},
   {"uk_c4", 8384},
   {"uk_five", 12866},
};


// Nothing user-configurable below this line.

static const char date_line[]       = "<TD id=\"date-box\" WIDTH=558 HEIGHT=23> &nbsp;";
static const char start_time_line[] = "<TD id=\"pitime-box\" WIDTH=48>";
static const char duration_line[]   = "<TD id=\"piminute-box\" WIDTH=40>";
static const char channel_line[]    = "<TD id=\"pisicon-box\" WIDTH=40><IMG SRC=\"/epg_fp/sender_images/";
static const char title_line[]      = "<TD id=\"pititle-box\" WIDTH=340><SPAN class=\"pititle-anker\"><a href=\"javascript:sh(";
static const char summary_line[]    = "<TD WIDTH=555 id=\"pitext-box\" style=\"width:550;margin:0px\">";
static const char subtitle_line[]   = "<TD id=\"pisubtitle-box\"";
static const char genre_line[]      = "<TD id=\"pigenre-box\" WIDTH=146>&nbsp;";

static const int CHANNEL_NOT_FOUND      = -1;
static const int CHANNEL_NOT_RECOGNIZED = -2;




char map_special_char(const char * const word)

{
   if (strcmp(word, "auml") == 0)
      return 'ä';
   else if (strcmp(word, "ouml") == 0)
      return 'ö';
   else if (strcmp(word, "uuml") == 0)
      return 'ü';
   else if (strcmp(word, "Auml") == 0)
      return 'Ä';
   else if (strcmp(word, "Ouml") == 0)
      return 'Ö';
   else if (strcmp(word, "Uuml") == 0)
      return 'Ü';
   else if (strcmp(word, "szlig") == 0)
      return 'ß';
   else if (strcmp(word, "nbsp") == 0)
      return ' ';
   else if (strcmp(word, "amp") == 0)
      return '&';
   else if (strcmp(word, "quot") == 0)
      return '"';
   return ' ';
}





void read_file_name_and_title(const char * const line, char * const file_name, char * const title)

{
   int line_index = sizeof(title_line) - 1;
   
   while ((line_index < max_line-max_title) && (line[line_index] != '>')) line_index++;
   line_index++;
   
   int name_index;
   for (name_index = 0; name_index < max_title-1; name_index++)
   {
      char ch = line[line_index++];
      if ((ch == '<') || (ch == 0)) break;
      if (ch == '&')
      {
         char word[10];
         int i = 0;
         while ((line[line_index + i] != ';') && (i < 9))
         {
            word[i] = line[line_index + i]; i++;
         }
         if (line[line_index + i] == ';')
         {
            word[i] = 0;
            ch = map_special_char(word);
            line_index += i + 1;
         }
      }
      switch (ch)
      {
         case ':':
            file_name[name_index] = '|';
         break;
         case '~':
            file_name[name_index] = '-';
         break;
         default:
            file_name[name_index] = ch;
      }
      title[name_index] = ch;
   };
   file_name[name_index] = 0;
   title[name_index] = 0;
}





void read_subtitle(const char * const line, char * const subtitle)

{
   int line_index = sizeof(subtitle_line) - 1;
   
   while ((line_index < max_line-max_title) && (line[line_index] != '>')) line_index++;
   line_index++;
   
   int name_index;
   for (name_index = 0; name_index < max_title-1; name_index++)
   {
      char ch = line[line_index++];
      if ((ch == '<') || (ch == 0)) break;
      if (ch == '&')
      {
         char word[10];
         int i = 0;
         while ((line[line_index + i] != ';') && (i < 9))
         {
            word[i] = line[line_index + i]; i++;
         }
         if (line[line_index + i] == ';')
         {
            word[i] = 0;
            ch = map_special_char(word);
            line_index += i + 1;
         }
      }
      subtitle[name_index] = ch;
   };
   subtitle[name_index] = 0;
}



void read_summary(const unsigned char * const line, char * const summary)

{
   int summary_index = 0;
   int ch;
   bool need_space = false;
   bool done = false;
   int line_index = sizeof(summary_line) - 1;
   const int line_length = strlen((const char *)line);

#define GETCHAR line_index < line_length? line[line_index++] : getchar()
   
   do
   {
      ch = GETCHAR;
      switch (ch)
      {
         case '&':
         {
            char word[10];
            int i = 0;
            ch = GETCHAR;
            while ((ch != ';') && (ch != EOF) && (i < 9))
            {
               word[i++] = ch;
               ch = GETCHAR;
            }
            word[i] = 0;
            if (need_space) {summary[summary_index++] = ' '; need_space = false;}
            summary[summary_index++] = map_special_char(word);
         }
         break;
         case '<':
         {
            char word[6];
            int word_index = 0;
            do
            {
               ch = GETCHAR;
               word[word_index++] = ch;
            } while ((word_index < 6) && (ch != '>') && (ch != EOF));
            while ((ch != '>') && (ch != EOF)) ch = GETCHAR;
            if (strncmp("/TD", word, 3) == 0)
               done = true;
         }
         break;
         default:
         {
            if (ch <= ' ')
            {
               if (summary_index > 0) need_space = true;
            }
            else
            {
               if (need_space) {summary[summary_index++] = ' '; need_space = false;}
               summary[summary_index++] = ch;
            }
         }
      }
   } while ((summary_index < max_summary - 2) && (!done) && (ch != EOF));
   summary[summary_index] = 0;
}




int find_channel_index(const vdr_channel_entry * const vdr_channel_data, const char * const channel_name)

{
   for (unsigned int tvtv_channel_number = 0; tvtv_channel_number < sizeof(channel_map)/sizeof(channel_map_entry); tvtv_channel_number++)
      if (strcmp(channel_name, channel_map[tvtv_channel_number].tvtv_name) == 0)
         for (int index = 0; vdr_channel_data[index].tid != -1; index++)
            if (vdr_channel_data[index].sid == channel_map[tvtv_channel_number].sid)
               return index;
   fprintf(stderr, "Error - channel '%s' not recognized.\n", channel_name);
   return CHANNEL_NOT_RECOGNIZED;
}




const char * find_file_name_prefix(const char * const title, const char * const genre)

{
   unsigned int i;

   for (i = 0; i < sizeof(title_map)/sizeof(name_map_entry); i++)
      if (strstr(title, title_map[i].string_to_be_matched) != NULL)
         return title_map[i].recording_prefix;

   for (i = 0; i < sizeof(genre_map)/sizeof(name_map_entry); i++)
      if (strstr(genre, genre_map[i].string_to_be_matched) != NULL)
         return genre_map[i].recording_prefix;

   return "";
}




vdr_channel_entry * read_vdr_channel_data(const char * const channels_conf_file_name)

{
   FILE * channels_conf = fopen(channels_conf_file_name, "r");
   if (channels_conf == NULL)
   {
      perror("unable to open channels.conf.");
      exit(1);
   }
   vdr_channel_entry * vdr_channel_data = (vdr_channel_entry *) malloc(max_vdr_channel * sizeof(vdr_channel_entry));
   int index = 0;
   while (!feof(channels_conf) && (index < max_vdr_channel-1))
   {
      char line[max_line];
      fgets(line, sizeof(line)-1, channels_conf);
      if (line[0] != ':')
      {
         int tid, sid, nid;
         char source[max_line];
         //                Name   Freq   Param  Sourc Srate  vpid   apid   tpid   ca     si ni ti
         if (sscanf(line, "%*[^:]:%*[^:]:%*[^:]:%[^:]:%*[^:]:%*[^:]:%*[^:]:%*[^:]:%*[^:]:%d:%d:%d", source, &sid, &nid, &tid) == 4)
         {
            vdr_channel_data[index].source = strdup(source);
            vdr_channel_data[index].sid = sid;
            vdr_channel_data[index].nid = nid;
            vdr_channel_data[index].tid = tid;
            index++;
         }
         else
            fprintf(stderr, "Error - %s line not recognized:\n  %s\nAre you using the old channels.conf format?\n\n",
                    channels_conf_file_name, line);
      }
   }
   vdr_channel_data[index++].tid = -1; // sentinel
   fprintf(stderr, "%d channel entries.\n", index);
   return (vdr_channel_entry *) realloc(vdr_channel_data, index * sizeof(vdr_channel_entry));
}




void process_input(const vdr_channel_entry * const vdr_channel_data,
                   const int recording_priority,
                   const int recording_lifetime)

{
   int timer_entries_created = 0;
   int channel               = CHANNEL_NOT_FOUND;
   int duration              = -1;
   unsigned int day          = 0;
   unsigned int month        = 0;
   unsigned int year         = 0;
   struct tm start_time      = {0, -1, -1, -1, -1, -1, 0, 0, -1};
   char genre[max_genre]     = {0};
   char summary[max_summary] = {0};
   char file_name[max_title] = {0};
   char title[max_title]     = {0};
   char subtitle[max_title]  = {0};
   
   const time_t now = time(NULL);

   while (!feof(stdin))
   {
      char line[max_line];
      fgets(line, max_line-1, stdin);
      line[max_line-1] = 0;

// date line. There may only be one of these for multiple entries, so we keep the data in "day", "month", "year".
      if (strncmp(line, date_line, sizeof(date_line)-1) == 0)
      {
         char month_name[10];
         sscanf(line + sizeof(date_line), "%u. %10[^ ] %u", &day, month_name, &year);
         for (month = 0; month <= 12 && strcmp(month_name, month_names[month]); month++);
         month++;
         if ((day < 1) || (day > 31) || (month < 1) || (month > 12) || (year < 2001) || (year > 2037))
         {
            fprintf(stderr, "Input data error - format error in date line:\n%s\n", line);
            exit(1);
         }
      }

// start time line.
      else if (strncmp(line, start_time_line, sizeof(start_time_line)-1) == 0)
      {
         if (start_time.tm_hour >= 0)
         {
            fprintf(stderr, "Input data error - duplicate start time line:\n%s\n", line);
            exit(1);
         }
         unsigned int hour = 99, minute = 99;
         sscanf(line + sizeof(start_time_line)-1, "%u.%u", &hour, &minute);
         if ((hour > 23) || (minute > 59))
         {
            fprintf(stderr, "Input data error - format error in start time line:\n%s\n", line);
            exit(1);
         }
         start_time.tm_hour = hour;
         start_time.tm_min = minute;
      }

// channel line.
      else if (strncmp(line, channel_line, sizeof(channel_line)-1) == 0)
      {
         if (channel >= 0)
         {
            fprintf(stderr, "Input data error - duplicate channel line:\n%s\n", line);
            exit(1);
         }
         int i = sizeof(channel_line);
         while ((line[i] != '.') && (line[i] != 0)) i++;
         line[i] = 0; // end of string
         channel = find_channel_index(vdr_channel_data, line + sizeof(channel_line) - 1);
      }

// duration line.
      else if (strncmp(line, duration_line, sizeof(duration_line)-1) == 0)
      {
         if (duration != -1)
         {
            fprintf(stderr, "Input data error - duplicate duration line:\n%s\n", line);
            exit(1);
         }
         if ((sscanf(line + sizeof(duration_line)-1, "%u", &duration) != 1) ||
             (duration >= 24*60) || (duration < 1))
         {
            fprintf(stderr, "Input data error - format error in duration line:\n%s\n", line);
            exit(1);
         }
      }

// genre line.
      else if (strncmp(line, genre_line, sizeof(genre_line)-1) == 0)
      {
         if (genre[0] != 0)
         {
            fprintf(stderr, "Input data error - duplicate genre line:\n%s\n", line);
            exit(1);
         }
         int genre_index;
         for (genre_index = 0; genre_index < max_genre-1; genre_index++)
         {
            const char ch = line[genre_index + sizeof(genre_line)-1];
            if ((ch == 0) || (ch == '&') || (ch == '<'))
               break;
            genre[genre_index] = ch;
         }
         genre[genre_index] = 0;
      }    

// title line.
      else if (strncmp(line, title_line, sizeof(title_line)-1) == 0)
      {
         if (file_name[0] != 0)
         {
            fprintf(stderr, "Input data error - duplicate title line:\n%s\n", line);
            exit(1);
         }
         read_file_name_and_title(line, file_name, title);
      }

// subtitle line.
      else if (strncmp(line, subtitle_line, sizeof(subtitle_line)-1) == 0)
      {
         if (subtitle[0] != 0)
         {
            fprintf(stderr, "Input data error - duplicate subtitle line:\n%s\n", line);
            exit(1);
         }
         read_subtitle(line, subtitle);
      }

// summary line. Last line of entry.
      else if (strncmp(line, summary_line, sizeof(summary_line)-1) == 0)
      {
         if (channel != CHANNEL_NOT_RECOGNIZED)
         {
            read_summary((const unsigned char *)line, summary);
            if (day == 0)
            {
               fprintf(stderr, "Input data error - date line not found.\n");
               exit(1);
            }
            if (start_time.tm_hour == -1)
            {
               fprintf(stderr, "Input data error - start time line not found.\n");
               exit(1);
            }
            if (duration == -1)
            {
               fprintf(stderr, "Input data error - duration line not found.\n");
               exit(1);
            }
            if (file_name[0] == 0)
            {
               fprintf(stderr, "Input data error - title line not found.\n");
               exit(1);
            }
            if (channel == CHANNEL_NOT_FOUND)
            {
               fprintf(stderr, "Input data error - channel line not found.\n");
               exit(1);
            }
            start_time.tm_mday = day;
            start_time.tm_mon = month-1;
            start_time.tm_year = year - 1900;

   // The tvtv notion of a day is a bit strange. "Today" lasts until tomorrow 05:59h.
   // The following line of code fixes that. mktime will take care of tm_mday > 31.
            if (start_time.tm_hour < 6) start_time.tm_mday++;

            start_time.tm_min -= start_time_safety_margin;
            start_time.tm_isdst = -1;
            if (mktime(&start_time) > now + max_time_processed) break;
            static const char start_string_format[] = "%d:%H%M";
            char start_string[sizeof start_string_format + 1];
            strftime(start_string, sizeof start_string_format, start_string_format, &start_time);

            start_time.tm_min += start_time_safety_margin + duration + stop_time_safety_margin;
            start_time.tm_isdst = -1;
            (void) mktime(&start_time);
            static const char stop_string_format[] = "%H%M";
            char stop_string[sizeof stop_string_format + 1];
            strftime(stop_string, sizeof stop_string_format, stop_string_format, &start_time);
            printf("1:%s-%d-%d-%d:%s:%s:%d:%d:%s%s%s:\"%s\" %s|%s||%s||||||(epg2timers)\n",
                   vdr_channel_data[channel].source, vdr_channel_data[channel].nid, vdr_channel_data[channel].tid, vdr_channel_data[channel].sid,
		   start_string[0] == '0'? start_string + 1 : start_string, stop_string,
                   recording_priority, recording_lifetime,
                   find_file_name_prefix(title, genre), file_name, episode,
                   title, genre, subtitle, summary);
            timer_entries_created++;
         }
         start_time.tm_sec = 0;
         start_time.tm_min = -1;
         start_time.tm_hour = -1;
         start_time.tm_mday = -1;
         start_time.tm_mon = -1;
         start_time.tm_year = -1;
         duration = -1;
         channel = CHANNEL_NOT_FOUND;
         file_name[0] = 0; summary[0] = 0; genre[0] = 0; subtitle[0] = 0;
      }
   }
   fprintf(stderr, "%d timer entries created.\n", timer_entries_created);

   if (timer_entries_created == 0) exit(1);
}




int main(int argc, char *argv[])

{
   fprintf(stderr, "epg2timers Version 1.3.6, 13-Apr-2004.\n");

   int recording_priority = 50;
   int recording_lifetime = 98;
   if (argc > 2) recording_priority = atoi(argv[2]);
   if (argc > 3) recording_lifetime = atoi(argv[3]);

   if ((argc < 2) || (argc > 4) ||
       (recording_priority < 0) || (recording_priority > 99) ||
       (recording_lifetime < 0) || (recording_lifetime > 99))
   {
      fprintf(stderr, "usage: %s channels.conf [priority [lifetime]]\n", argv[0]);
      exit(1);
   }

   const vdr_channel_entry * const vdr_channel_data = read_vdr_channel_data(argv[1]);
   process_input(vdr_channel_data, recording_priority, recording_lifetime);
   return 0;
}
