/*
 * exec: A plugin for the Video Disk Recorder
 * Copyright (C) 2007  Winfried Koehler
 * Licence GPL
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id$
 */

#include <vdr/thread.h>
#if VDRVERSNUM > 10501
/* shutdown.h was introduced in 1.5.1 */
#include <vdr/shutdown.h>
#endif //VDRVERSNUM < 10501
#include "menu.h"
#include "exectimer.h"
#include "compat.h"

cExecTimers ExecTimers;
cExecLog    ExecLog;

/****************************************************************************** 
 * some helper funcs here
 *****************************************************************************/

time_t Now(void) { // Get the current time
 time_t now; 
 return time(&now);
}

/****************************************************************************** 
 * cTaskThread
 *****************************************************************************/

cTaskThread::cTaskThread(int * State, int * LogState) {
  active=true;
  state = State;
  logstate = LogState;
  Start();
}

cTaskThread::~cTaskThread(void) {
  active=false;
}

void cTaskThread::Action(void) {
  while (active && Running()) {
    cw.Wait(1000);
    ExecTimers.CheckTimers();
    ExecTimers.DeleteExpired();
    if (ExecTimers.Modified(* state)) {
      log(4, "exec: saving timers.");
      ExecTimers.Save();
      }
    if (ExecLog.Modified(* logstate)) {
      ExecLog.Save();
      }   
    }
}

/****************************************************************************** 
 * cExecTimer
 *****************************************************************************/

cExecTimer::cExecTimer(time_t StartTime, const char * Command, int TimeOut) {
  startTime=StartTime;
  asprintf(&command,Command);
  asprintf(&wd_str,"AAAAAAA");
  timeout = TimeOut;
  active  = true;
  running = false;
  started = false;
  flags   = ET_DEFAULT;
  weekdays= WD_IGNORED;
}

cExecTimer::~cExecTimer() {
  if (command) free(command);
  if (wd_str)  free(wd_str);
}

bool cExecTimer::Execute(void) {
  bool background=!(flags & ET_FOREGROUND); /* default to true */
  long long action = flags & ET_MASK_ACT;

  switch (action) {
    case ET_SHELLCOMMAND:
      int retval;
      started=true;
      running=true;
      log(EXEC_LOG + 3,"exec shell command: %s", command);
      retval=SysExec(command, background);
      running=false;
      if (!retval) {
        log(EXEC_LOG + 3,"success: %s", command);
        return true;
        }
      log(EXEC_LOG + 0,"%s: %s",
        retval==-1?"FAILED":retval==127?"NOT FOUND":
        retval==126?"NO PERMISSIONS":"ERROR", command);
      return false;
      break;
    case ET_SHUTDOWN:
      running=true;
      log(EXEC_LOG  + 3,"exec: shutdown!");
      return DoShutdown();
      break;
    case ET_MESSAGE:
      running=true;
      log(EXEC_LOG + 3,"message %s", command);
      Skins.Message(mtInfo, command, 30);
      return true;
      break;
    case ET_WARNING:
      running=true;
      log(EXEC_LOG + 3,"warning %s", command);
      Skins.Message(mtWarning, command, 30);
      return true;
      break;
    default:/* should never happen */      
      log(EXEC_LOG + 0, "Execute() unknown action %llu!", action);
      return false; 
    }
}

bool cExecTimer::Expired() {
  if ((StartTime()+timeout+60) < Now()) {
    running=false;
    started=false;
    }
  if (weekdays & WD_MASK) return false;
  return (((StartTime()+120) < Now()) && !running);
}

cString cExecTimer::ToSVDRDescr() const {
  /* output has to be readable by Parse() !! */
  char *buffer;
  int  Action;

  switch (flags & ET_MASK_ACT) {
    case ET_SHELLCOMMAND : Action=1; break;
    case ET_SHUTDOWN     : Action=2; break;
    case ET_MESSAGE      : Action=3; break;
    case ET_WARNING      : Action=4; break;
    default: Action=1;
    } 
  asprintf(&buffer,"%s:%s:%d:%d:%d:%d:%d:%s ",
    *PrintWeekdays(), *PrintTime(false), timeout, active?1:0,
    Action, (flags & ET_WAKEUP)?1:0, (flags & ET_BACKGROUND)?1:0,
    command);
  return cString(buffer, true);
}

cString cExecTimer::PrintWeekdays() const {
  char *buffer;
  if (weekdays == WD_IGNORED)
    asprintf(&buffer, "%s", *PrintDay());
  else
    asprintf(&buffer, "%s%s%s%s%s%s%s",
       (weekdays & WD_MONDAY)?    "M":"-",
       (weekdays & WD_TUESDAY)?   "T":"-",
       (weekdays & WD_WEDNESDAY)? "W":"-",
       (weekdays & WD_THURSDAY)?  "T":"-",
       (weekdays & WD_FRIDAY)?    "F":"-",
       (weekdays & WD_SATURDAY)?  "S":"-",
       (weekdays & WD_SUNDAY)?    "S":"-");
  return cString(buffer, true);
}

cString cExecTimer::PrintDay() const {
  char *buffer;
  if (weekdays == WD_IGNORED)
    asprintf(&buffer, "%04d-%02d-%02d",
       Day / 10000,
      (Day % 10000) / 100,
      (Day % 10000) % 100);
  else
    asprintf(&buffer, "----:--:--");
  return cString(buffer, true);
}

cString cExecTimer::PrintTime(bool colon) const {
  char *buffer;
  asprintf(&buffer, "%02d%s%02d",
       Time / 100,
       colon?":":"",
       Time % 100);
  return cString(buffer, true);
}

void cExecTimer::SetTime(time_t t, int offset) {
  tm local;
  log(4, "SetTime %d, %d", t, offset);
  startTime=t+offset;
  // we now have to set Day and Time again.
  local=*(localtime(&startTime));
  Day  = (local.tm_year + 1900)*10000 + (local.tm_mon + 1)*100 + local.tm_mday;
  Time = local.tm_hour*100 + local.tm_min;
}

cString cExecTimer::Command() const {
  char *buffer;
  asprintf(&buffer, "%s", command);
  return cString(buffer, true);
}

time_t cExecTimer::StartTime() {
  time_t t;
  tm     local, st;
  bool   found=false;
  int    wd = 0;

  time(&t);                        /* put current time into curr  */
  local=*(localtime(&t));          /* dereference and assign      */
  st   =*(localtime(&startTime));  /* dereference and assign      */
  
  if (weekdays == WD_IGNORED)      /* single shot timer           */  
    return startTime;
  /* MTWTFSS */
  for (int i=0; i <= 7; i++) {     /* when will this timer run next? */
    wd = (local.tm_wday + i);
    if (wd > 6) wd -= 7; /* saturday -> sunday */
    switch (wd) {
      case 0: found = (weekdays & WD_SUNDAY);    break;
      case 1: found = (weekdays & WD_MONDAY);    break;
      case 2: found = (weekdays & WD_TUESDAY);   break;
      case 3: found = (weekdays & WD_WEDNESDAY); break;
      case 4: found = (weekdays & WD_THURSDAY);  break;
      case 5: found = (weekdays & WD_FRIDAY);    break;
      case 6: found = (weekdays & WD_SATURDAY);  break;
      default: log(0, "StartTime(): invalid weekday in wd");  
      }
    if (found) {
      local.tm_mday=(local.tm_mday + i);
      local.tm_hour = st.tm_hour;
      local.tm_min  = st.tm_min;
      local.tm_sec  = 0;
      t=mktime(&local);
      if (Now() > (t+60)) {
        /* timer runs on the weekday found,
         * but this time is already in past
         *  -> continue searching next exec time
         */
        found=false;
        continue;
        }
      return t;
      }
    }
  return 0; // remark: this is time in past, 01.01.1970
}

bool cExecTimer::Parse(const char * s) {
char DayStr[8   + 64]; // 64 chars reserved
char CmdStr[256 + 64]; // 64 chars reserved
int Action, WakeUp, Detach, Active;
time_t curr;
tm     local;

  if (8 == sscanf(s, "%[^:]:%d:%d:%d:%d:%d:%d:%[^:\n]",
      DayStr,&Time,&timeout,&Active,&Action,&WakeUp,&Detach,CmdStr)) {
    time(&curr);               // get current time_t value
    local=*(localtime(&curr)); // dereference and assign
    if (! CmdStr || ! DayStr) {
      log(0, "Parse() invalid Pointer!");
      return false;
      }
    if (strlen(CmdStr) < 1) {
      log(0, "invalid parameter <COMMAND>");
      return false;
      }
    if (strlen(DayStr) < 1) {
      log(0, "invalid parameter <DAY>");
      return false;
      }
    if (command) free(command);
    asprintf(&command,"%s", CmdStr);
    weekdays = WD_IGNORED;
    switch (strlen(DayStr)) {
      case 10:  /* YYYY-MM-DD */
         if (3 == sscanf(DayStr, "%04d-%02d-%02d",
             &local.tm_year,&local.tm_mon,&local.tm_mday)) {
           local.tm_year -= 1900;
           local.tm_mon  -= 1;
           }
         else {
           log(0, "invalid parameter <DAY>");
           return false;
           }
         break;                
      case 7:   /* MTWTFSS    */
         if (strncasecmp(DayStr+0, "-", 1)) weekdays += WD_MONDAY;
         if (strncasecmp(DayStr+1, "-", 1)) weekdays += WD_TUESDAY;
         if (strncasecmp(DayStr+2, "-", 1)) weekdays += WD_WEDNESDAY;
         if (strncasecmp(DayStr+3, "-", 1)) weekdays += WD_THURSDAY;
         if (strncasecmp(DayStr+4, "-", 1)) weekdays += WD_FRIDAY;
         if (strncasecmp(DayStr+5, "-", 1)) weekdays += WD_SATURDAY;
         if (strncasecmp(DayStr+6, "-", 1)) weekdays += WD_SUNDAY;
         break;
      case 5:  /* MM-DD      */
         if (2 == sscanf(DayStr, "%02d-%02d",
             &local.tm_mon,&local.tm_mday)) {
           local.tm_mon  -= 1;
           }
         else {
           log(0, "invalid parameter <DAY>");
           return false;
           }
         break;
      case 2:   /* -1;DD      */
         if (1 == sscanf(DayStr, "%02d", &Day)) {
           switch (Day) {
             case -1:        flags = (flags | ET_IGNORE_DATE);
                             break; /* now */

             case 1 ... 31:  local.tm_mday=Day;
                             break;

             default:        log(0, "invalid parameter <DAY>");
                             return false;
             }
           }
         else {
           log(0, "invalid parameter <DAY>");
           return false;
           }
         break;
      default:  log(0, "invalid parameter <DAY>"); return false;
      } 
    Day = (local.tm_year + 1900)*10000 + (local.tm_mon + 1)*100 + local.tm_mday;
    switch (Time) {
      case -1      : // now
                     flags = (flags | ET_IGNORE_TIME);
                     break;
      case 0000 ... 2359:
                     // hhmm
                     local.tm_hour=(int)(Time / 100);
                     Time=Time - ((int)(Time / 100)*100);
                     if (Time>=0 && Time<60)
                       local.tm_min=Time;
                     else {
                       log(0, "exec: Parse(): invalid min %d", Time);
                       return false;
                       }
                     break;
      default:       log(0, "exec: Parse(): invalid Time %d", Time);
                     return false;
      }
    Time = local.tm_hour*100 + local.tm_min; 
    local.tm_sec=0;
    active=Active?true:false;
    flags  = ET_NONE;
    Action = 1 << (Action>0?Action-1:0);
    flags  += (long long) (Action << 8);
    if (WakeUp > 0)
      flags += ET_WAKEUP;
    if (Detach > 0)
      flags += ET_BACKGROUND;
    }
  else {
    log(0, "Parse(): wrong number of arguments, expected 8 args");
    return false;
    }
  startTime=mktime(&local);
  return true;                           
}                                           

bool cExecTimer::Save(FILE *f) {
  return fprintf(f, "%s \n", *ToSVDRDescr()) > 0;
}


/****************************************************************************** 
 * cExecTimers
 *****************************************************************************/

cExecTimers::cExecTimers(void) {
  state = 0;
  beingEdited = 0;
  lastDeleteExpired = 0;
}

bool cExecTimers::Modified(int &State) {
  bool Result = state != State;
  State = state;
  return Result;
}

void cExecTimers::DeleteExpired(void) {
  if (beingEdited)
    return;
  if (time(NULL) - lastDeleteExpired < 30)
    return;
  if (time(NULL) - lastTimerStart < 30)
    return;
  cExecTimer *et = First();
  while (et) {
    cExecTimer *next = Next(et);
    if (! et) continue; // should NEVER happen!
    if (et->Expired()) {
      log(EXEC_LOG + 3,"timer expired: %s", *et->ToSVDRDescr());
      lastDeleteExpired=Now();
      Del(et);
      SetModified();
      break;
      }
    et = next;
    }
  lastDeleteExpired = time(NULL);
}

void cExecTimers::CheckTimers(void) {
  time_t now = Now();

  for (cExecTimer *et = First(); et; et = Next(et)) {
    if (! et || ! et->isActive()) continue;
    if ((now >= et->StartTime()) && (now <= (et->StartTime() + 60) )) {
      if (! et->wasStarted()) {/* start only once */
        et->Execute();
        SetTimerStart();
        }
      }
    }
}

cExecTimer *cExecTimers::GetNextActiveTimer(void) {
  cExecTimer *et0 = NULL;
  time_t st = 0;
  
  for (cExecTimer *et = First(); et; et = Next(et)) {
    if (! et || ! et->isActive()) continue;
    if (et->StartTime() >= Now()) {
      if (st == 0) st = et->StartTime();
      if (et->StartTime() <= st) {
        st = et->StartTime();
        et0 = et;
        }
      }
    }
  return et0;
}

cExecTimer *cExecTimers::GetNextWakeupTimer(void) {
  cExecTimer *et0 = NULL;
  time_t st = 0;
  
  for (cExecTimer *et = First(); et; et = Next(et)) {
    if (! et || ! et->isActive()) continue;
    if (!(et->Flags() & ET_WAKEUP)) continue;
    if (et->StartTime() >= Now()) {
      if (st == 0) st = et->StartTime();
      if (et->StartTime() <= st) {
        st = et->StartTime();
        et0 = et;
        }
      }
    }
  return et0;
}


/****************************************************************************** 
 * cExecLogLine
 *****************************************************************************/

cExecLogLine::cExecLogLine(const char * Str) {
  asprintf(&str,Str);
}

cExecLogLine::~cExecLogLine() {
  if (str) free(str);
}

cString cExecLogLine::Text(void) const {
  char *buffer;
  asprintf(&buffer, "%s", str);
  return cString(buffer, true);
}

bool cExecLogLine::Parse(const char * s) {
  if (str) free(str);
  asprintf(&str,"%s", s);
  return true;                           
}                                           

bool cExecLogLine::Save(FILE *f) {
  return fprintf(f, "%s\n", *Text()) > 0;
}

/****************************************************************************** 
 * cExecLog
 *****************************************************************************/

cExecLog::cExecLog(void) {
  state = 0;
  beingEdited = 0;
}

bool cExecLog::Modified(int &State) {
  bool Result = state != State;
  State = state;
  return Result;
}

void cExecLog::AddToLog(const char * Str, ...) {
  IncBeingEdited();
  cExecLogLine *line=new cExecLogLine(Str);
  Add(line);
  while (Count() > ExecSetup.logLength) {
    cExecLogLine *l = First();
    Del(l);
    }
  DecBeingEdited();
  SetModified();
}

