/*
 * timers.c: Timer handling
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: timers.c 5.18 2022/11/20 10:57:31 kls Exp $
 */

#include "timers.h"
#include <ctype.h>
#include "device.h"
#include "i18n.h"
#include "libsi/si.h"
#include "recording.h"
#include "remote.h"
#include "status.h"
#include "svdrp.h"

// IMPORTANT NOTE: in the 'sscanf()' calls there is a blank after the '%d'
// format characters in order to allow any number of blanks after a numeric
// value!

// --- cTimer ----------------------------------------------------------------

cTimer::cTimer(bool Instant, bool Pause, const cChannel *Channel)
{
  id = 0;
  startTime = stopTime = 0;
  scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
  deferred = 0;
  pending = inVpsMargin = false;
  flags = tfNone;
  *pattern = 0;
  *file = 0;
  aux = NULL;
  remote = NULL;
  event = NULL;
  if (Instant)
     SetFlags(tfActive | tfInstant);
  LOCK_CHANNELS_READ;
  channel = Channel ? Channel : Channels->GetByNumber(cDevice::CurrentChannel());
  time_t t = time(NULL);
  struct tm tm_r;
  struct tm *now = localtime_r(&t, &tm_r);
  day = SetTime(t, 0);
  weekdays = 0;
  start = now->tm_hour * 100 + now->tm_min;
  stop = 0;
  if (!Setup.InstantRecordTime && channel && (Instant || Pause)) {
     LOCK_SCHEDULES_READ;
     if (const cSchedule *Schedule = Schedules->GetSchedule(channel)) {
        if (const cEvent *Event = Schedule->GetPresentEvent()) {
           time_t tstart = Event->StartTime();
           time_t tstop = Event->EndTime();
           if (Event->Vps() && Setup.UseVps) {
              SetFlags(tfVps);
              tstart = Event->Vps();
              }
           else {
              int MarginStart = 0;
              int MarginStop  = 0;
              CalcMargins(MarginStart, MarginStop, Event);
              tstart -= MarginStart;
              tstop  += MarginStop;
              }
           day = SetTime(tstart, 0);
           struct tm *time = localtime_r(&tstart, &tm_r);
           start = time->tm_hour * 100 + time->tm_min;
           time = localtime_r(&tstop, &tm_r);
           stop = time->tm_hour * 100 + time->tm_min;
           SetEvent(Event);
           }
        }
     }
  if (!stop) {
     stop = now->tm_hour * 60 + now->tm_min + (Setup.InstantRecordTime ? Setup.InstantRecordTime : DEFINSTRECTIME);
     stop = (stop / 60) * 100 + (stop % 60);
     }
  if (stop >= 2400)
     stop -= 2400;
  priority = Pause ? Setup.PausePriority : Setup.DefaultPriority;
  lifetime = Pause ? Setup.PauseLifetime : Setup.DefaultLifetime;
  if (Instant && channel)
     snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name());
}

static bool MatchPattern(const char *Pattern, const char *Title, cString *Before = NULL, cString *Match = NULL, cString *After = NULL)
{
  if (Title) {
     bool AvoidDuplicates = startswith(Pattern, TIMERPATTERN_AVOID);
     if (AvoidDuplicates)
        Pattern++;
     if (strcmp(Pattern, "*") == 0) {
        if (Before)
           *Before = "";
        if (Match)
           *Match = Title;
        if (After)
           *After = "";
        return true;
        }
     bool AnchorBegin = startswith(Pattern, TIMERPATTERN_BEGIN);
     if (AnchorBegin)
        Pattern++;
     bool AnchorEnd = endswith(Pattern, TIMERPATTERN_END);
     cNullTerminate nt;
     if (AnchorEnd)
        nt.Set(const_cast<char *>(Pattern + strlen(Pattern) - 1));
     if (AnchorBegin && AnchorEnd) {
        if (strcmp(Title, Pattern) == 0) {
           if (Before)
              *Before = "";
           if (Match)
              *Match = Title;
           if (After)
              *After = "";
           return true;
           }
        }
     else if (AnchorBegin) {
        if (strstr(Title, Pattern) == Title) {
           if (Before)
              *Before = "";
           if (Match)
              *Match = Pattern;
           if (After)
              *After = cString(Title + strlen(Pattern));
           return true;
           }
        }
     else if (AnchorEnd) {
        if (endswith(Title, Pattern)) {
           if (Before)
              *Before = cString(Title, Title + strlen(Title) - strlen(Pattern));
           if (Match)
              *Match = Pattern;
           if (After)
              *After = "";
           return true;
           }
        }
     else if (const char *p = strstr(Title, Pattern)) {
        if (Before)
           *Before = cString(Title, p);
        if (Match)
           *Match = Pattern;
        if (After)
           *After = cString(p + strlen(Pattern));
        return true;
        }
     }
  return false;
}

static cString MakePatternFileName(const char *Pattern, const char *Title, const char *Episode, const char *File)
{
  if (!Pattern || !Title || !File)
     return NULL;
  cString Before = "";
  cString Match = "";
  cString After = "";
  if (MatchPattern(Pattern, Title, &Before, &Match, &After)) {
     char *Result = strdup(File);
     Result = strreplace(Result, TIMERMACRO_TITLE, Title);
     if (!isempty(Episode)) // the event might not yet have a "short text", so we leave this to the actual recording
        Result = strreplace(Result, TIMERMACRO_EPISODE, Episode);
     Result = strreplace(Result, TIMERMACRO_BEFORE, Before);
     Result = strreplace(Result, TIMERMACRO_MATCH, Match);
     Result = strreplace(Result, TIMERMACRO_AFTER, After);
     return cString(Result, true);
     }
  return NULL;
}

cTimer::cTimer(const cEvent *Event, const char *FileName, const cTimer *PatternTimer)
{
  id = 0;
  startTime = stopTime = 0;
  scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
  deferred = 0;
  pending = inVpsMargin = false;
  flags = tfActive;
  *pattern = 0;
  *file = 0;
  aux = NULL;
  remote = NULL;
  event = NULL;
  if (!PatternTimer || PatternTimer->HasFlags(tfVps)) {
     if (Event->Vps() && (PatternTimer || Setup.UseVps))
        SetFlags(tfVps);
     }
  LOCK_CHANNELS_READ;
  channel = Channels->GetByChannelID(Event->ChannelID(), true);
  time_t tstart = (flags & tfVps) ? Event->Vps() : Event->StartTime();
  time_t tstop = tstart + Event->Duration();
  if (!(HasFlags(tfVps))) {
     int MarginStart = 0;
     int MarginStop  = 0;
     CalcMargins(MarginStart, MarginStop, Event);
     tstart -= MarginStart;
     tstop  += MarginStop;
     }
  struct tm tm_r;
  struct tm *time = localtime_r(&tstart, &tm_r);
  day = SetTime(tstart, 0);
  weekdays = 0;
  start = time->tm_hour * 100 + time->tm_min;
  time = localtime_r(&tstop, &tm_r);
  stop = time->tm_hour * 100 + time->tm_min;
  if (stop >= 2400)
     stop -= 2400;
  priority = PatternTimer ? PatternTimer->Priority() : Setup.DefaultPriority;
  lifetime = PatternTimer ? PatternTimer->Lifetime() : Setup.DefaultLifetime;
  if (!FileName)
     FileName = Event->Title();
  if (!isempty(FileName))
     Utf8Strn0Cpy(file, FileName, sizeof(file));
  SetEvent(Event);
}

cTimer::cTimer(const cTimer &Timer)
{
  channel = NULL;
  aux = NULL;
  remote = NULL;
  event = NULL;
  flags = tfNone;
  *this = Timer;
}

cTimer::~cTimer()
{
  if (event)
     event->DecNumTimers();
  free(aux);
  free(remote);
}

cTimer& cTimer::operator= (const cTimer &Timer)
{
  if (&Timer != this) {
     id           = Timer.id;
     startTime    = Timer.startTime;
     stopTime     = Timer.stopTime;
     scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
     deferred     = 0;
     pending      = Timer.pending;
     inVpsMargin  = Timer.inVpsMargin;
     flags        = Timer.flags;
     channel      = Timer.channel;
     day          = Timer.day;
     weekdays     = Timer.weekdays;
     start        = Timer.start;
     stop         = Timer.stop;
     priority     = Timer.priority;
     lifetime     = Timer.lifetime;
     strncpy(pattern, Timer.pattern, sizeof(pattern));
     strncpy(file, Timer.file, sizeof(file));
     free(aux);
     aux = Timer.aux ? strdup(Timer.aux) : NULL;
     free(remote);
     remote = Timer.remote ? strdup(Timer.remote) : NULL;
     if (event)
        event->DecNumTimers();
     event = Timer.event;
     if (event)
        event->IncNumTimers();
     }
  return *this;
}

void cTimer::CalcMargins(int &MarginStart, int &MarginStop, const cEvent *Event)
{
  MarginStart = Setup.MarginStart * 60;
  MarginStop  = Setup.MarginStop * 60;
  // To make sure the timer gets assigned to the correct event, we must
  // make sure that this is the only event that overlaps 100%:
  if (const cEvent *e = dynamic_cast<const cEvent *>(Event->Prev()))
     MarginStart = max(0, min(MarginStart, e->Duration() - 60));
  if (const cEvent *e = dynamic_cast<const cEvent *>(Event->Next()))
     MarginStop = max(0, min(MarginStop, e->Duration() - 60));
}

int cTimer::Compare(const cListObject &ListObject) const
{
  const cTimer *ti = (const cTimer *)&ListObject;
  time_t t1 = StartTime();
  time_t t2 = ti->StartTime();
  int r = t1 - t2;
  if (r == 0)
     r = ti->priority - priority;
  if (IsPatternTimer() ^ ti->IsPatternTimer()) {
     if (IsPatternTimer())
        r = 1;
     else
        r = -1;
     }
  else if (IsPatternTimer() && ti->IsPatternTimer())
     r = strcoll(Pattern(), ti->Pattern());
  return r;
}

cString cTimer::PatternAndFile(void) const
{
  if (IsPatternTimer())
     return cString::sprintf("{%s}%s", pattern, file);
  return file;
}

cString cTimer::ToText(bool UseChannelID) const
{
  strreplace(pattern, ':', '|');
  strreplace(file, ':', '|');
  cString buffer = cString::sprintf("%u:%s:%s:%04d:%04d:%d:%d:%s:%s", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, weekdays, true), start, stop, priority, lifetime, *PatternAndFile(), aux ? aux : "");
  strreplace(pattern, '|', ':');
  strreplace(file, '|', ':');
  return buffer;
}

cString cTimer::ToDescr(void) const
{
  return cString::sprintf("%d%s%s (%d %04d-%04d %s'%s')", Id(), remote ? "@" : "", remote ? remote : "", Channel()->Number(), start, stop, HasFlags(tfVps) ? "VPS " : "", *PatternAndFile());
}

int cTimer::TimeToInt(int t)
{
  return (t / 100 * 60 + t % 100) * 60;
}

bool cTimer::ParseDay(const char *s, time_t &Day, int &WeekDays)
{
  // possible formats are:
  // 19
  // 2005-03-19
  // MTWTFSS
  // MTWTFSS@19
  // MTWTFSS@2005-03-19

  Day = 0;
  WeekDays = 0;
  s = skipspace(s);
  if (!*s)
     return false;
  const char *a = strchr(s, '@');
  const char *d = a ? a + 1 : isdigit(*s) ? s : NULL;
  if (d) {
     if (strlen(d) == 10) {
        struct tm tm_r;
        if (3 == sscanf(d, "%d-%d-%d", &tm_r.tm_year, &tm_r.tm_mon, &tm_r.tm_mday)) {
           tm_r.tm_year -= 1900;
           tm_r.tm_mon--;
           tm_r.tm_hour = tm_r.tm_min = tm_r.tm_sec = 0;
           tm_r.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
           Day = mktime(&tm_r);
           }
        else
           return false;
        }
     else {
        // handle "day of month" for compatibility with older versions:
        char *tail = NULL;
        int day = strtol(d, &tail, 10);
        if (tail && *tail || day < 1 || day > 31)
           return false;
        time_t t = time(NULL);
        int DaysToCheck = 61; // 61 to handle months with 31/30/31
        for (int i = -1; i <= DaysToCheck; i++) {
            time_t t0 = IncDay(t, i);
            if (GetMDay(t0) == day) {
               Day = SetTime(t0, 0);
               break;
               }
            }
        }
     }
  if (a || !isdigit(*s)) {
     if ((a && a - s == 7) || strlen(s) == 7) {
        for (const char *p = s + 6; p >= s; p--) {
            WeekDays <<= 1;
            WeekDays |= (*p != '-');
            }
        }
     else
        return false;
     }
  return true;
}

cString cTimer::PrintDay(time_t Day, int WeekDays, bool SingleByteChars)
{
#define DAYBUFFERSIZE 64
  char buffer[DAYBUFFERSIZE];
  char *b = buffer;
  if (WeekDays) {
     // TRANSLATORS: the first character of each weekday, beginning with monday
     const char *w = trNOOP("MTWTFSS");
     if (!SingleByteChars)
        w = tr(w);
     while (*w) {
           int sl = Utf8CharLen(w);
           if (WeekDays & 1) {
              for (int i = 0; i < sl; i++)
                  b[i] = w[i];
              b += sl;
              }
           else
              *b++ = '-';
           WeekDays >>= 1;
           w += sl;
           }
     if (Day)
        *b++ = '@';
     }
  if (Day) {
     struct tm tm_r;
     localtime_r(&Day, &tm_r);
     b += strftime(b, DAYBUFFERSIZE - (b - buffer), "%Y-%m-%d", &tm_r);
     }
  *b = 0;
  return buffer;
}

cString cTimer::PrintFirstDay(void) const
{
  if (weekdays) {
     cString s = PrintDay(day, weekdays, true);
     if (strlen(s) == 18)
        return *s + 8;
     }
  return ""; // not NULL, so the caller can always use the result
}

bool cTimer::Parse(const char *s)
{
  char *channelbuffer = NULL;
  char *daybuffer = NULL;
  char *filebuffer = NULL;
  free(aux);
  aux = NULL;
  //XXX Apparently sscanf() doesn't work correctly if the last %m argument
  //XXX results in an empty string (this first occurred when the EIT gathering
  //XXX was put into a separate thread - don't know why this happens...
  //XXX As a cure we copy the original string and add a blank.
  //XXX If anybody can shed some light on why sscanf() fails here, I'd love
  //XXX to hear about that!
  char *s2 = NULL;
  int l2 = strlen(s);
  while (l2 > 0 && isspace(s[l2 - 1]))
        l2--;
  if (s[l2 - 1] == ':') {
     s2 = MALLOC(char, l2 + 3);
     strcat(strn0cpy(s2, s, l2 + 1), " \n");
     s = s2;
     }
  bool result = false;
  if (8 <= sscanf(s, "%u :%m[^:]:%m[^:]:%d :%d :%d :%d :%m[^:\n]:%m[^\n]", &flags, &channelbuffer, &daybuffer, &start, &stop, &priority, &lifetime, &filebuffer, &aux)) {
     if (aux && !*skipspace(aux)) {
        free(aux);
        aux = NULL;
        }
     //TODO add more plausibility checks
     result = ParseDay(daybuffer, day, weekdays);
     char *fb = filebuffer;
     if (*fb == '{') {
        if (char *p = strchr(fb, '}')) {
           *p = 0;
           Utf8Strn0Cpy(pattern, fb + 1, sizeof(pattern));
           strreplace(pattern, '|', ':');
           fb = p + 1;
           }
        }
     else
        *pattern = 0;
     Utf8Strn0Cpy(file, fb, sizeof(file));
     strreplace(file, '|', ':');
     LOCK_CHANNELS_READ;
     if (isnumber(channelbuffer))
        channel = Channels->GetByNumber(atoi(channelbuffer));
     else
        channel = Channels->GetByChannelID(tChannelID::FromString(channelbuffer), true, true);
     if (!channel) {
        esyslog("ERROR: channel %s not defined", channelbuffer);
        result = false;
        }
     }
  free(channelbuffer);
  free(daybuffer);
  free(filebuffer);
  free(s2);
  return result;
}

bool cTimer::Save(FILE *f)
{
  if (!Remote())
     return fprintf(f, "%s\n", *ToText(true)) > 0;
  return true;
}

bool cTimer::IsSingleEvent(void) const
{
  return !weekdays;
}

int cTimer::GetMDay(time_t t)
{
  struct tm tm_r;
  return localtime_r(&t, &tm_r)->tm_mday;
}

int cTimer::GetWDay(time_t t)
{
  struct tm tm_r;
  int weekday = localtime_r(&t, &tm_r)->tm_wday;
  return weekday == 0 ? 6 : weekday - 1; // we start with Monday==0!
}

bool cTimer::DayMatches(time_t t) const
{
  return IsSingleEvent() ? SetTime(t, 0) == day : (weekdays & (1 << GetWDay(t))) != 0;
}

time_t cTimer::IncDay(time_t t, int Days)
{
  struct tm tm_r;
  tm tm = *localtime_r(&t, &tm_r);
  tm.tm_mday += Days; // now tm_mday may be out of its valid range
  int h = tm.tm_hour; // save original hour to compensate for DST change
  tm.tm_isdst = -1;   // makes sure mktime() will determine the correct DST setting
  t = mktime(&tm);    // normalize all values
  tm.tm_hour = h;     // compensate for DST change
  return mktime(&tm); // calculate final result
}

time_t cTimer::SetTime(time_t t, int SecondsFromMidnight)
{
  struct tm tm_r;
  tm tm = *localtime_r(&t, &tm_r);
  tm.tm_hour = SecondsFromMidnight / 3600;
  tm.tm_min = (SecondsFromMidnight % 3600) / 60;
  tm.tm_sec =  SecondsFromMidnight % 60;
  tm.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
  return mktime(&tm);
}

void cTimer::SetPattern(const char *Pattern)
{
  Utf8Strn0Cpy(pattern, Pattern, sizeof(pattern));
}

void cTimer::SetFile(const char *File)
{
  if (!isempty(File))
     Utf8Strn0Cpy(file, File, sizeof(file));
}

#define EITPRESENTFOLLOWINGRATE 10 // max. seconds between two occurrences of the "EIT present/following table for the actual multiplex" (2s by the standard, using some more for safety)

bool cTimer::Matches(time_t t, bool Directly, int Margin) const
{
  startTime = stopTime = 0;
  if (t == 0)
     t = time(NULL);

  int begin  = TimeToInt(start); // seconds from midnight
  int end    = TimeToInt(stop);
  int length = end - begin;

  if (IsSingleEvent()) {
     time_t t0 = day;
     startTime = SetTime(t0, begin);
     if (length < 0)
        t0 = IncDay(day, 1);
     stopTime  = SetTime(t0, end);
     }
  else {
     time_t d = day ? max(day, t) : t;
     for (int i = -1; i <= 7; i++) {
         time_t t0 = IncDay(d, i);
         if (DayMatches(t0)) {
            time_t a = SetTime(t0, begin);
            if (length < 0)
               t0 = IncDay(d, i + 1);
            time_t b = SetTime(t0, end);
            if ((!day || a >= day) && t < b) {
               startTime = a;
               stopTime = b;
               break;
               }
            }
         }
     if (!startTime)
        startTime = IncDay(t, 7); // just to have something that's more than a week in the future
     else if (!Directly && (t > startTime || t > day + SECSINDAY + 3600)) // +3600 in case of DST change
        day = 0;
     }

  if (IsPatternTimer())
     return false; // we only need to have start/stopTime initialized

  if (t < deferred)
     return false;
  deferred = 0;

  if (HasFlags(tfActive)) {
     if (event) {
        if (HasFlags(tfVps)) {
           if (event->Vps()) {
              if (Margin || !Directly) {
                 startTime = event->StartTime();
                 stopTime = event->EndTime();
                 if (!Margin) { // this is an actual check
                    if (event->Schedule()->PresentSeenWithin(EITPRESENTFOLLOWINGRATE)) // VPS control can only work with up-to-date events...
                       return event->IsRunning(true);
                    // ...otherwise we fall back to normal timer handling below (note: Margin == 0!)
                    }
                 }
              }
           }
        else if (HasFlags(tfSpawned)) {
           if (!Margin && !Directly) { // this is an actual check
              // The spawned timer's start-/stopTimes are adjusted to the event's times in AdjustSpawnedTimer().
              // However, in order to make sure the timer is set to the correct event, the margins at begin
              // end end are limited by the durations of the events before and after this timer's event.
              // The recording, though, shall always use the full start/stop margins, hence this calculation:
              return event->StartTime() - Setup.MarginStart * 60 <= t && t < event->EndTime() + Setup.MarginStop * 60;
              }
           }
        }
     return startTime <= t + Margin && t < stopTime; // must stop *before* stopTime to allow adjacent timers
     }
  return false;
}

#define FULLMATCH 1000

eTimerMatch cTimer::Matches(const cEvent *Event, int *Overlap) const
{
  // Overlap is the percentage of the Event's duration that is covered by
  // this timer (based on FULLMATCH for finer granularity than just 100).
  // To make sure a VPS timer can be distinguished from a plain 100% overlap,
  // it gets an additional 100 added, and a VPS event that is actually running
  // gets 200 added to the FULLMATCH.
  if (channel->GetChannelID() == Event->ChannelID()) {
     bool UseVps = HasFlags(tfVps) && Event->Vps();
     if (IsPatternTimer()) {
        if (startswith(Pattern(), TIMERPATTERN_AVOID)) {
           cString FileName = MakePatternFileName(Pattern(), Event->Title(), Event->ShortText(), File());
           if (*FileName) {
              const char *p = strgetlast(*FileName, FOLDERDELIMCHAR);
              if (DoneRecordingsPattern.Contains(p))
                 return tmNone;
              }
           else
              return tmNone;
           }
        else if (!MatchPattern(Pattern(), Event->Title()))
           return tmNone;
        UseVps = false;
        }
     Matches(UseVps ? Event->Vps() : Event->StartTime(), true);
     int overlap = 0;
     if (UseVps) {
        if (startTime == Event->Vps()) {
           overlap = FULLMATCH;
           if (Event->IsRunning())
              overlap += 200;
           else if (Event->RunningStatus() != SI::RunningStatusNotRunning)
              overlap += 100;
           }
        }
     else {
        if (startTime <= Event->StartTime() && Event->EndTime() <= stopTime)
           overlap = FULLMATCH;
        else if (stopTime <= Event->StartTime() || Event->EndTime() <= startTime)
           overlap = 0;
        else {
           overlap = (min(stopTime, Event->EndTime()) - max(startTime, Event->StartTime())) * FULLMATCH / max(Event->Duration(), 1);
           if (IsPatternTimer() && overlap > 0)
              overlap = FULLMATCH;
           }
        }
     startTime = stopTime = 0;
     if (Overlap)
        *Overlap = overlap;
     return overlap >= FULLMATCH ? tmFull : overlap > 0 ? tmPartial : tmNone;
     }
  return tmNone;
}

#define EXPIRELATENCY 60 // seconds (just in case there's a short glitch in the VPS signal)

bool cTimer::Expired(void) const
{
  if (IsSingleEvent() && !Recording()) {
     time_t ExpireTime = StopTimeEvent();
     if (HasFlags(tfVps))
        ExpireTime += EXPIRELATENCY;
     return ExpireTime <= time(NULL);
     }
  return false;
}

time_t cTimer::StartTime(void) const
{
  if (!startTime)
     Matches();
  return startTime;
}

time_t cTimer::StopTime(void) const
{
  if (!stopTime)
     Matches();
  return stopTime;
}

time_t cTimer::StartTimeEvent(void) const
{
  if (event) {
     if (HasFlags(tfVps) && event->Vps())
        return event->StartTime();
     else if (HasFlags(tfSpawned))
        return event->StartTime() - Setup.MarginStart * 60;
     }
  return StartTime();
}

time_t cTimer::StopTimeEvent(void) const
{
  if (event) {
     if (HasFlags(tfVps) && event->Vps())
        return event->EndTime();
     else if (HasFlags(tfSpawned))
        return event->EndTime() + Setup.MarginStop * 60;
     }
  return StopTime();
}

#define EPGLIMITBEFORE   (1 * 3600) // Time in seconds before a timer's start time and
#define EPGLIMITAFTER    (1 * 3600) // after its stop time within which EPG events will be taken into consideration.

void cTimer::SetId(int Id)
{
  id = Id;
}

cTimer *cTimer::SpawnPatternTimer(const cEvent *Event, cTimers *Timers)
{
  cString FileName = MakePatternFileName(Pattern(), Event->Title(), Event->ShortText(), File());
  isyslog("spawning timer %s for event %s", *ToDescr(), *Event->ToDescr());
  cTimer *t = new cTimer(Event, FileName, this);
  t->SetFlags(tfSpawned);
  if (startswith(Pattern(), TIMERPATTERN_AVOID))
     t->SetFlags(tfAvoid);
  Timers->Add(t);
  HandleRemoteTimerModifications(t);
  return t;
}

bool cTimer::SpawnPatternTimers(const cSchedules *Schedules, cTimers *Timers)
{
  bool TimersSpawned = false;
  const cSchedule *Schedule = Schedules->GetSchedule(Channel());
  if (Schedule && Schedule->Events()->First()) {
     if (Schedule->Modified(scheduleStateSpawn)) {
        time_t Now = time(NULL);
        // Find the first event that matches this pattern timer and either already has a spawned
        // timer, or has not yet ended:
        for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
            if (Matches(e) != tmNone) {
               const cTimer *Timer = Timers->GetTimerForEvent(e, tfSpawned); // a matching event that already has a spawned timer
               if (!Timer && e->EndTime() > Now) { // only look at events that have not yet ended
                  Timer = SpawnPatternTimer(e, Timers);
                  TimersSpawned = true;
                  }
               if (Timer) {
                  // Check all following matching events that would start while the first timer
                  // is still recording:
                  bool UseVps = Timer->HasFlags(tfVps);
                  time_t Limit = Timer->StopTimeEvent();
                  if (UseVps)
                     Limit += EXPIRELATENCY;
                  else
                     Limit += Setup.MarginStart * 60;
                  for (e = Schedule->Events()->Next(e); e; e = Schedule->Events()->Next(e)) {
                      if (e->StartTime() <= Limit) {
                         if (!Timers->GetTimerForEvent(e, tfSpawned) && Matches(e) != tmNone) {
                            SpawnPatternTimer(e, Timers);
                            TimersSpawned = true;
                            }
                         if (UseVps)
                            break; // with VPS we only need to check the event immediately following the first one
                         }
                      else
                         break; // no need to check events that are too far in the future
                      }
                  break;
                  }
               }
            }
        }
     }
  return TimersSpawned;
}

bool cTimer::AdjustSpawnedTimer(void)
{
  if (Event()) {
     if (const cSchedule *Schedule = Event()->Schedule()) { // events may be deleted from their schedule in cSchedule::DropOutdated()!
        if (Schedule->Modified(scheduleStateAdjust)) {
           // Adjust the timer to shifted start/stop times of the event if necessary:
           time_t tstart = Event()->StartTime();
           time_t tstop = Event()->EndTime();
           int MarginStart = 0;
           int MarginStop  = 0;
           CalcMargins(MarginStart, MarginStop, Event());
           tstart -= MarginStart;
           tstop  += MarginStop;
           // Event start/end times are given in "seconds since the epoch". Some broadcasters use values
           // that result in full minutes (with zero seconds), while others use any values. VDR's timers
           // use times given in full minutes, truncating any seconds. Thus we only react if the start/stop
           // times of the timer are off by at least one minute:
           if (abs(StartTime() - tstart) >= 60 || abs(StopTime() - tstop) >= 60) {
              cString OldDescr = ToDescr();
              struct tm tm_r;
              struct tm *time = localtime_r(&tstart, &tm_r);
              SetDay(cTimer::SetTime(tstart, 0));
              SetStart(time->tm_hour * 100 + time->tm_min);
              time = localtime_r(&tstop, &tm_r);
              SetStop(time->tm_hour * 100 + time->tm_min);
              Matches();
              isyslog("timer %s times changed to %s-%s", *OldDescr, *TimeString(tstart), *TimeString(tstop));
              return true;
              }
           }
        }
     }
  return false;
}

void cTimer::TriggerRespawn(void)
{
  if (Local() && HasFlags(tfSpawned) || IsPatternTimer()) {
     if (Channel()) {
        LOCK_CHANNELS_READ;
        if (const cSchedule *Schedule = Channel()->Schedule()) {
           dsyslog("triggering respawn for timer %s", *ToDescr());
           LOCK_SCHEDULES_WRITE;
           const_cast<cSchedule *>(Schedule)->SetModified();
           }
        }
     }
}

bool cTimer::SetEventFromSchedule(const cSchedules *Schedules)
{
  if (IsPatternTimer())
     return SetEvent(NULL);
  const cSchedule *Schedule = Schedules->GetSchedule(Channel());
  if (Schedule && Schedule->Events()->First()) {
     if (Schedule->Modified(scheduleStateSet)) {
        const cEvent *Event = NULL;
        if (HasFlags(tfVps) && Schedule->Events()->First()->Vps()) {
           // VPS timers only match if their start time exactly matches the event's VPS time:
           for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
               if (e->StartTime()) {
                  int overlap = 0;
                  if (Matches(e, &overlap) == tmFull) {
                     Event = e;
                     if (overlap > FULLMATCH)
                        break; // take the first matching event
                     }
                  }
               }
           }
        else {
           // Normal timers match the event they have the most overlap with:
           int Overlap = 0;
           // Set up the time frame within which to check events:
           Matches(0, true);
           time_t TimeFrameBegin = StartTime() - EPGLIMITBEFORE;
           time_t TimeFrameEnd   = StopTime()  + EPGLIMITAFTER;
           for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) {
               if (e->EndTime() < TimeFrameBegin)
                  continue; // skip events way before the timer starts
               if (e->StartTime() > TimeFrameEnd)
                  break; // the rest is way after the timer ends
               int overlap = 0;
               Matches(e, &overlap);
               if (overlap && overlap >= Overlap) {
                  if (Event && overlap == Overlap && e->Duration() <= Event->Duration())
                     continue; // if overlap is the same, we take the longer event
                  Overlap = overlap;
                  Event = e;
                  }
               }
           }
        return SetEvent(Event);
        }
     }
  return false;
}

bool cTimer::SetEvent(const cEvent *Event)
{
  if (event != Event) {
     if (event)
        event->DecNumTimers();
     if (Event) {
        isyslog("timer %s set to event %s", *ToDescr(), *Event->ToDescr());
        Event->IncNumTimers();
        Event->Schedule()->Modified(scheduleStateSet); // to get the current state
        }
     else {
        isyslog("timer %s set to no event", *ToDescr());
        scheduleStateSet = scheduleStateSpawn = scheduleStateAdjust = -1;
        }
     event = Event;
     return true;
     }
  return false;
}

void cTimer::SetRecording(bool Recording)
{
  if (Recording)
     SetFlags(tfRecording);
  else
     ClrFlags(tfRecording);
  isyslog("timer %s %s", *ToDescr(), Recording ? "start" : "stop");
}

void cTimer::SetPending(bool Pending)
{
  pending = Pending;
}

void cTimer::SetInVpsMargin(bool InVpsMargin)
{
  if (InVpsMargin && !inVpsMargin)
     isyslog("timer %s entered VPS margin", *ToDescr());
  inVpsMargin = InVpsMargin;
}

void cTimer::SetDay(time_t Day)
{
  day = Day;
}

void cTimer::SetWeekDays(int WeekDays)
{
  weekdays = WeekDays;
}

void cTimer::SetStart(int Start)
{
  start = Start;
}

void cTimer::SetStop(int Stop)
{
  stop = Stop;
}

void cTimer::SetPriority(int Priority)
{
  priority = Priority;
}

void cTimer::SetLifetime(int Lifetime)
{
  lifetime = Lifetime;
}

void cTimer::SetAux(const char *Aux)
{
  free(aux);
  aux = Aux ? strdup(Aux) : NULL;
}

void cTimer::SetRemote(const char *Remote)
{
  free(remote);
  remote = Remote ? strdup(Remote) : NULL;
}

void cTimer::SetDeferred(int Seconds)
{
  deferred = time(NULL) + Seconds;
  isyslog("timer %s deferred for %d seconds", *ToDescr(), Seconds);
}

void cTimer::SetFlags(uint Flags)
{
  flags |= Flags;
}

void cTimer::ClrFlags(uint Flags)
{
  flags &= ~Flags;
}

void cTimer::InvFlags(uint Flags)
{
  flags ^= Flags;
}

bool cTimer::HasFlags(uint Flags) const
{
  return (flags & Flags) == Flags;
}

void cTimer::Skip(void)
{
  day = IncDay(SetTime(StartTime(), 0), 1);
  startTime = 0;
  SetEvent(NULL);
}

void cTimer::OnOff(void)
{
  if (IsSingleEvent() || IsPatternTimer())
     InvFlags(tfActive);
  else if (day) {
     day = 0;
     ClrFlags(tfActive);
     }
  else if (HasFlags(tfActive))
     Skip();
  else
     SetFlags(tfActive);
  SetEvent(NULL);
  if (HasFlags(tfActive))
     TriggerRespawn(); // have pattern timers spawn if necessary
  Matches(); // refresh start and end time
}

// --- cTimers ---------------------------------------------------------------

cTimers cTimers::timers;
int cTimers::lastTimerId = 0;

cTimers::cTimers(void)
:cConfig<cTimer>("1 Timers")
{
  lastDeleteExpired = 0;
}

bool cTimers::Load(const char *FileName)
{
  LOCK_TIMERS_WRITE;
  Timers->SetExplicitModify();
  if (timers.cConfig<cTimer>::Load(FileName)) {
     for (cTimer *ti = timers.First(); ti; ti = timers.Next(ti)) {
         ti->SetId(NewTimerId());
         ti->ClrFlags(tfRecording);
         Timers->SetModified();
         }
     return true;
     }
  return false;
}

int cTimers::NewTimerId(void)
{
  return ++lastTimerId; // no need for locking, the caller must have a lock on the global Timers list
}

const cTimer *cTimers::GetById(int Id, const char *Remote) const
{
  for (const cTimer *ti = First(); ti; ti = Next(ti)) {
      if (ti->Id() == Id) {
         if (!Remote && !ti->Remote() || Remote && ti->Remote() && strcmp(Remote, ti->Remote()) == 0)
            return ti;
         }
      }
  return NULL;
}

const cTimer *cTimers::GetTimer(const cTimer *Timer) const
{
  for (const cTimer *ti = First(); ti; ti = Next(ti)) {
      if (!ti->Remote() &&
          ti->Channel() == Timer->Channel() &&
          (ti->WeekDays() && ti->WeekDays() == Timer->WeekDays() || !ti->WeekDays() && ti->Day() == Timer->Day()) &&
          ti->Start() == Timer->Start() &&
          ti->Stop() == Timer->Stop())
         return ti;
      }
  return NULL;
}

const cTimer *cTimers::GetMatch(time_t t) const
{
  static int LastPending = -1;
  const cTimer *t0 = NULL;
  for (const cTimer *ti = First(); ti; ti = Next(ti)) {
      if (!ti->Remote() && !ti->Recording() && ti->Matches(t)) {
         if (ti->Pending()) {
            if (ti->Index() > LastPending) {
               LastPending = ti->Index();
               return ti;
               }
            else
               continue;
            }
         if (!t0 || ti->Priority() > t0->Priority())
            t0 = ti;
         }
      }
  if (!t0)
     LastPending = -1;
  return t0;
}

const cTimer *cTimers::GetMatch(const cEvent *Event, eTimerMatch *Match) const
{
  const cTimer *t = NULL;
  eTimerMatch m = tmNone;
  for (const cTimer *ti = First(); ti; ti = Next(ti)) {
      eTimerMatch tm = ti->Matches(Event);
      if (tm > m || tm == tmFull && t && (t->Remote() && ti->Local() || t->IsPatternTimer() && ti->HasFlags(tfSpawned))) {
         t = ti;
         m = tm;
         }
      }
  if (Match)
     *Match = m;
  return t;
}

const cTimer *cTimers::GetTimerForEvent(const cEvent *Event, eTimerFlags Flags) const
{
  if (Event && Event->HasTimer()) {
     for (const cTimer *ti = First(); ti; ti = Next(ti)) {
         if (ti->Event() == Event && ti->Local() && ti->HasFlags(Flags))
            return ti;
         }
     }
  return NULL;
}

int cTimers::GetMaxPriority(void) const
{
  int n = -1;
  for (const cTimer *ti = First(); ti; ti = Next(ti)) {
      if (!ti->Remote() && ti->Recording())
         n = max(n, ti->Priority());
      }
  return n;
}

const cTimer *cTimers::GetNextActiveTimer(void) const
{
  const cTimer *t0 = NULL;
  for (const cTimer *ti = First(); ti; ti = Next(ti)) {
      if (!ti->Remote() && !ti->IsPatternTimer()) {
         ti->Matches();
         if ((ti->HasFlags(tfActive)) && (!t0 || ti->StopTime() > time(NULL) && ti->Compare(*t0) < 0))
            t0 = ti;
         }
      }
  return t0;
}

const cTimers *cTimers::GetTimersRead(cStateKey &StateKey, int TimeoutMs)
{
  return timers.Lock(StateKey, false, TimeoutMs) ? &timers : NULL;
}

cTimers *cTimers::GetTimersWrite(cStateKey &StateKey, int TimeoutMs)
{
  return timers.Lock(StateKey, true, TimeoutMs) ? &timers : NULL;
}

void cTimers::Add(cTimer *Timer, cTimer *After)
{
  if (!Timer->Remote())
     Timer->SetId(NewTimerId());
  cConfig<cTimer>::Add(Timer, After);
  cStatus::MsgTimerChange(Timer, tcAdd);
}

void cTimers::Ins(cTimer *Timer, cTimer *Before)
{
  cConfig<cTimer>::Ins(Timer, Before);
  cStatus::MsgTimerChange(Timer, tcAdd);
}

void cTimers::Del(cTimer *Timer, bool DeleteObject)
{
  cStatus::MsgTimerChange(Timer, tcDel);
  cConfig<cTimer>::Del(Timer, DeleteObject);
}

const cTimer *cTimers::UsesChannel(const cChannel *Channel) const
{
  for (const cTimer *Timer = First(); Timer; Timer = Next(Timer)) {
      if (Timer->Channel() == Channel)
         return Timer;
      }
  return NULL;
}

bool cTimers::SetEvents(const cSchedules *Schedules)
{
  bool TimersModified = false;
  for (cTimer *ti = First(); ti; ti = Next(ti)) {
      if (!ti->IsPatternTimer())
         TimersModified |= ti->SetEventFromSchedule(Schedules);
      }
  return TimersModified;
}

bool cTimers::SpawnPatternTimers(const cSchedules *Schedules)
{
  bool TimersModified = false;
  for (cTimer *ti = First(); ti; ti = Next(ti)) {
      if (ti->IsPatternTimer() && ti->Local()) {
         if (ti->HasFlags(tfActive))
            TimersModified |= ti->SpawnPatternTimers(Schedules, this);
         }
      }
  return TimersModified;
}

bool cTimers::AdjustSpawnedTimers(void)
{
  bool TimersModified = false;
  for (cTimer *ti = First(); ti; ti = Next(ti)) {
      if (ti->Local()) {
         if (ti->HasFlags(tfSpawned) && !ti->HasFlags(tfVps))
            TimersModified |= ti->AdjustSpawnedTimer();
         }
      }
  return TimersModified;
}

#define DELETE_EXPIRED_TIMEOUT  30 // seconds

bool cTimers::DeleteExpired(bool Force)
{
  if (!Force && time(NULL) - lastDeleteExpired < DELETE_EXPIRED_TIMEOUT)
     return false;
  bool TimersModified = false;
  cTimer *ti = First();
  while (ti) {
        cTimer *next = Next(ti);
        if (!ti->Remote() && ti->Expired()) {
           ti->SetEvent(NULL); // Del() doesn't call ~cTimer() right away, so this is necessary here
           ti->TriggerRespawn(); // in case this is a spawned timer
           isyslog("deleting timer %s", *ti->ToDescr());
           Del(ti);
           TimersModified = true;
           }
        ti = next;
        }
  lastDeleteExpired = time(NULL);
  return TimersModified;
}

bool cTimers::StoreRemoteTimers(const char *ServerName, const cStringList *RemoteTimers)
{
  bool Result = false;
  if (!ServerName || !RemoteTimers || RemoteTimers->Size() == 0) {
     // Remove remote timers from this list:
     cTimer *Timer = First();
     while (Timer) {
           cTimer *t = Next(Timer);
           if (Timer->Remote() && (!ServerName || strcmp(Timer->Remote(), ServerName) == 0)) {
              Del(Timer);
              Result = true;
              }
           Timer = t;
           }
     return Result;
     }
  // Collect all locally stored remote timers from ServerName:
  cStringList tl;
  for (cTimer *ti = First(); ti; ti = Next(ti)) {
      if (ti->Remote() && strcmp(ti->Remote(), ServerName) == 0)
         tl.Append(strdup(cString::sprintf("%d %s", ti->Id(), *ti->ToText(true))));
      }
  tl.SortNumerically(); // RemoteTimers is also sorted numerically!
  // Compare the two lists and react accordingly:
  int il = 0; // index into the local ("left") list of remote timers
  int ir = 0; // index into the remote ("right") list of timers
  int sl = tl.Size();
  int sr = RemoteTimers->Size();
  for (;;) {
      int AddTimer = 0;
      int DelTimer = 0;
      if (il < sl) { // still have left entries
         int nl = atoi(tl[il]);
         if (ir < sr) { // still have right entries
            // Compare timers:
            int nr = atoi((*RemoteTimers)[ir]);
            if (nl == nr) // same timer id
               AddTimer = DelTimer = nl;
            else if (nl < nr) // left entry not in right list
               DelTimer = nl;
            else // right entry not in left list
               AddTimer = nr;
            }
         else // processed all right entries
            DelTimer = nl;
         }
      else if (ir < sr) { // still have right entries
         AddTimer = atoi((*RemoteTimers)[ir]);
         if (!AddTimer) {
            esyslog("ERROR: %s: error in timer settings: %s", ServerName, (*RemoteTimers)[ir]);
            ir++;
            continue; // let's see if we can process the rest
            }
         }
      else // processed all left and right entries
         break;
      if (AddTimer && DelTimer) {
         if (strcmp(tl[il], (*RemoteTimers)[ir]) != 0) {
            // Overwrite timer:
            char *v = (*RemoteTimers)[ir];
            while (*v && *v != ' ')
                  v++; // skip id
            if (cTimer *l = GetById(DelTimer, ServerName)) {
               cTimer r;
               if (r.Parse(v)) {
                  r.SetRemote(ServerName);
                  r.SetId(AddTimer);
                  *l = r;
                  Result = true;
                  }
               else
                  esyslog("ERROR: %d@%s: error in timer settings: %s", DelTimer, ServerName, v);
               }
            }
         else // identical timer, nothing to do
            ;
         il++;
         ir++;
         }
      else if (AddTimer) {
         char *v = (*RemoteTimers)[ir];
         while (*v && *v != ' ')
               v++; // skip id
         cTimer *Timer = new cTimer;
         if (Timer->Parse(v)) {
            Timer->SetRemote(ServerName);
            Timer->SetId(AddTimer);
            Add(Timer);
            Result = true;
            }
         else {
            esyslog("ERROR: %s: error in timer settings: %s", ServerName, v);
            delete Timer;
            }
         ir++;
         }
      else if (DelTimer) {
         if (cTimer *t = GetById(DelTimer, ServerName)) {
            Del(t);
            Result = true;
            }
         il++;
         }
      else {
         esyslog("ERROR: oops while storing remote timers!");
         break; // let's not get stuck here!
         }
      }
  return Result;
}

static bool RemoteTimerError(const cTimer *Timer, cString *Msg)
{
  if (Msg)
     *Msg = cString::sprintf("%s %d@%s!", tr("Error while accessing remote timer"), Timer->Id(), Timer->Remote());
  return false; // convenience return code
}

bool HandleRemoteTimerModifications(cTimer *NewTimer, cTimer *OldTimer, cString *Msg)
{
  cStringList Response;
  if (!NewTimer) {
     if (OldTimer) { // timer shall be deleted from remote machine
        if (OldTimer->Remote() && OldTimer->Id()) {
           if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
              return RemoteTimerError(OldTimer, Msg);
           }
        isyslog("deleted timer %s", *OldTimer->ToDescr());
        }
     }
  else if (!OldTimer || OldTimer->Local() || !OldTimer->Id()) {
     if (NewTimer->Local()) { // timer stays local, nothing to do
        if (OldTimer && OldTimer->Id())
           isyslog("modified timer %s", *NewTimer->ToDescr());
        else
           isyslog("added timer %s", *NewTimer->ToDescr());
        }
     else { // timer is new, or moved from local to remote
        if (!ExecSVDRPCommand(NewTimer->Remote(), cString::sprintf("NEWT %s", *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
           return RemoteTimerError(NewTimer, Msg);
        int RemoteId = atoi(SVDRPValue(Response[0]));
        if (RemoteId <= 0)
           return RemoteTimerError(NewTimer, Msg);
        NewTimer->SetId(RemoteId);
        if (OldTimer && OldTimer->Id()) {
           isyslog("moved timer %d to %s", OldTimer->Id(), *NewTimer->ToDescr());
           }
        else
           isyslog("added timer %s", *NewTimer->ToDescr());
        }
     }
  else if (NewTimer->Local()) { // timer is moved from remote to local
     if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
        return RemoteTimerError(OldTimer, Msg);
     NewTimer->SetId(cTimers::NewTimerId());
     NewTimer->ClrFlags(tfRecording); // in case it was recording on the remote machine
     isyslog("moved timer %d@%s to %s", OldTimer->Id(), OldTimer->Remote(), *NewTimer->ToDescr());
     }
  else if (strcmp(OldTimer->Remote(), NewTimer->Remote()) == 0) { // timer stays remote on same machine
     if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("MODT %d %s", OldTimer->Id(), *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
        return RemoteTimerError(NewTimer, Msg);
     isyslog("modified timer %s", *NewTimer->ToDescr());
     }
  else { // timer is moved from one remote machine to an other
     if (!ExecSVDRPCommand(NewTimer->Remote(), cString::sprintf("NEWT %s", *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
        return RemoteTimerError(NewTimer, Msg);
     int RemoteId = atoi(SVDRPValue(Response[0]));
     if (RemoteId <= 0)
        return RemoteTimerError(NewTimer, Msg);
     NewTimer->SetId(RemoteId);
     if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
        return RemoteTimerError(OldTimer, Msg);
     isyslog("moved timer %d@%s to %s", OldTimer->Id(), OldTimer->Remote(), *NewTimer->ToDescr());
     }
  return true;
}

// --- cSortedTimers ---------------------------------------------------------

static int CompareTimers(const void *a, const void *b)
{
  return (*(const cTimer **)a)->Compare(**(const cTimer **)b);
}

cSortedTimers::cSortedTimers(const cTimers *Timers)
:cVector<const cTimer *>(Timers->Count())
{
  for (const cTimer *Timer = Timers->First(); Timer; Timer = Timers->Next(Timer))
      Append(Timer);
  Sort(CompareTimers);
}
