/*
 * update.c: TVTV plugin for the Video Disk Recorder
 *
 * See the README file for copyright information and how to reach the author.
 *
 */

#include "update.h"



cUpdate::cUpdate()
{
    active = false;
    data.memory = NULL;
    data.size = 0;
}

cUpdate::~cUpdate()
{
    if (active)
    {
	active = false;
	Cancel(3);
    }

    if (data.memory) {
        free(data.memory);
        data.memory = NULL;
        data.size = 0;
    }

}

void cUpdate::StartUpdate()
{
    if (!active)
	Start();
    else
    {
	active = false;
	Cancel(3);
	usleep(250);
	Start();
    }
}

void cUpdate::Action(void)
{
    dsyslog("TVTV: Timer Thread started (pid=%d)", getpid());
    
    active = true;

    do    
    {
	MakeTimerUpdate();

	if (TVTVConfig.autoupdate && TVTVConfig.updatetime > 0)
	    for (long i = 0; active && i < TVTVConfig.updatetime * 60 * 2; i++)
		usleep(500000);
    }
    while (active && TVTVConfig.autoupdate && TVTVConfig.updatetime > 0);
    
    active = false;
    
    dsyslog("TVTV: Timer Thread ended (pid=%d)", getpid());
    
    Cancel(0);
}


static size_t WriteMemoryCallback( void *ptr, size_t size, size_t nmemb, void *data) {
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)data;

    if (mem->memory) 
        mem->memory = (char *)realloc(mem->memory, mem->size + realsize + 1);
    else
	mem->memory = (char *)malloc(mem->size + realsize + 1);
    
    if (mem->memory) {
        memcpy(&(mem->memory[mem->size]), ptr, realsize);
        mem->size += realsize;
        mem->memory[mem->size] = 0;
    }
    return realsize;
}


int cUpdate::DownloadCSVData(const char *url) {
    CURL *curl_handle;

    // Initialize 'data' struct
    if (data.memory) free(data.memory);
    data.memory = NULL;
    data.size = 0;
    
    if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
        esyslog("TVTV: Something went wrong with curl_global_init()");
	return -1;
    }
    curl_handle = curl_easy_init();
    if (curl_handle == NULL) {
        esyslog("TVTV: Unable to get handle from curl_easy_init()");
	return -1;
    }
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);  // Specify URL to get
    
    if (TVTVConfig.useproxy) {
        curl_easy_setopt(curl_handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
	curl_easy_setopt(curl_handle, CURLOPT_PROXY, TVTVConfig.httpproxy);  // Specify HTTP proxy
    }

    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);  // Send all data to this function
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&data);  // Pass our 'data' struct to the callback function
    curl_easy_setopt(curl_handle, CURLOPT_MAXFILESIZE, 1048576);  // Set maximum file size to get (bytes)
    curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);  // No progress meter
    curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);  // No signaling
    curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 30);  // Set timeout to 30 seconds
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, TVTV_USERAGENT);  // Some servers don't like requests that are made without a user-agent field
    
    if (curl_easy_perform(curl_handle) != 0) {
	curl_easy_cleanup(curl_handle);  // Cleanup curl stuff
	if (data.memory) {
	    free(data.memory);
	    data.memory = NULL;
	    data.size = 0;
	}
	esyslog("TVTV: Couldn't download the stream");
	return -1;
    }


    // Debug
    ofstream logfile( "/var/log/vdr_tvtv.log", ios::app );
    logfile << data.memory;
    logfile.close();
    
    curl_easy_cleanup(curl_handle);  // Cleanup curl stuff
    return data.size;
}


uint8_t toHex(const uint8_t &x)
{
    return x > 9 ? x + 55: x + 48;
}
    
void URLEncode(const char *sIn, char *sOut)
{
    register char *pOutTmp = NULL;
    char *pOutBuf = NULL;
    register char *pInTmp = NULL;
    char *pInBuf = (char *)sIn;
    
    //alloc out buffer
    pOutBuf = sOut;
	
    if(pOutBuf)
    {
        pInTmp	= pInBuf;
        pOutTmp = pOutBuf;
				    
	// do encoding
	while (*pInTmp)
	{
	    if(isalnum(*pInTmp))
	        *pOutTmp++ = *pInTmp;
	    else
	        if(isspace(*pInTmp))
		    *pOutTmp++ = '+';
	        else
	        {
	            *pOutTmp++ = '%';
		    *pOutTmp++ = toHex(*pInTmp>>4);
	            *pOutTmp++ = toHex(*pInTmp%16);
		}
	    pInTmp++;
	}
	*pOutTmp = '\0';
    }
}


bool cUpdate::MakeTimerUpdate(void)
{
    char *sEnCryptS = NULL;
    char *url = NULL;
    
    sEnCryptS = (char *)calloc(100, sizeof (char));
    URLEncode(sPackedString, sEnCryptS);    

    asprintf(&url, "http://%s/cgi-bin/WebObjects/TVSync.woa/wa/getjobs?&serial=0&account=%s&product=35&target=%s&doctype=csv&access=%s",
             TVTV_SERVERS[TVTVConfig.tvtv_server], TVTVConfig.username, TVTV_SERVERS[TVTVConfig.tvtv_server], sEnCryptS);

    isyslog("TVTV: Timer Update started");
    
    int iFileSize = DownloadCSVData(url);
    
    if (iFileSize > 0)
    {
	dsyslog("TVTV: Received %d Bytes", iFileSize);
	/* process CSV file */
	ProcessImportedFile(data.memory);
    
	isyslog("TVTV: Timer File processed");
    }
    else
	isyslog("TVTV: Download Error");
   
    // Clean up
    if (data.memory) {
	free(data.memory);
	data.memory = NULL;
	data.size = 0;
    }
    if (url) free(url);
    if (sEnCryptS) free(sEnCryptS);
    
    return true;
}

int cUpdate::FindChannelSID(const char *sChannel)
{
    int iReturn = -1;
    
    int iMaxChannel = sizeof(channel_map) / sizeof(channel_map_entry);
    for (int i = 0; iReturn == -1 && i < iMaxChannel; i++)
	if (strcmp(sChannel, channel_map[i].tvtv_name) == 0)
	    iReturn = channel_map[i].pnr;
	    
    return iReturn;
}

cChannel *cUpdate::GetChannelByServiceID(int ServiceID)
{
    for (cChannel *channel = Channels.First(); channel; channel = Channels.Next(channel))
    {
	if (!channel->GroupSep() && channel->Sid() == ServiceID)
	    return channel;
    }
    
    return NULL;
}
			 
int cUpdate::calc_field_cnt(string *s) {
  int n=0;
  size_t p = 0;

  if ((s == NULL) || (s->empty())) return 0;

  p = s->find(',', 0);
  while (p != string::npos) {
    n++;
    p = s->find(',', p+1);
  }
  n++;
  return n;
}

char *cUpdate::strip_str(char *s) {
  char *m=NULL;
  unsigned int i=0;

  if (s==NULL) return NULL;

  m=(char *)calloc(strlen(s)+1, sizeof(char));

  for (i=0; i<strlen(s); i++) {
    if (s[i] == '"') {
      if (s[i+1] == '"') {
	strncat(m, s+i, 1);
	i++;
      }
    } else if ((s[i] == '\\')&&(s[i+1] == 'n')) {
      strncat(m, "\n", 1);
      i++;
    } else {
      strncat(m, s+i, 1);
    }
  }
  return m;
}


char **cUpdate::split_csv(const char *job_line, int field_cnt) {

  char **arr=NULL;
  char *sp=NULL, *ep=NULL;
  char *s=NULL;
  int i=0;
  int n=0;
  int fini=0;

  if ((job_line == NULL)||(field_cnt == 0)) { return NULL; }

  arr=(char **)calloc(field_cnt, sizeof(char *));
  sp=(char *)job_line;
  
  while ((n < field_cnt)&&(fini == 0)) {
    if (*sp == '"') {
      sp++;
      ep=index(sp, '"');
      if (ep != NULL) {
        while ( *(ep+1) == '"' ) { ep=index(ep+2, '"'); }
        ep++;
      } else {
 	ep=(char *)job_line + strlen(job_line);
	fini=1;
      }
    } else {
      ep=index(sp, ',');
      if (ep == NULL) {
	ep=(char *)job_line + strlen(job_line);
	fini=1;
      }
    }
    s=(char *)calloc(ep-sp+1, sizeof(char));
    strncpy(s, sp, ep-sp);
    arr[n]=strip_str(s);
    free(s);
    
    sp=ep+1;
    n++;
    
    if ((n < field_cnt)&&(fini==1)) {
      for(i=0; i<n; i++) free(arr[i]);
      free(arr);
      arr=NULL;
    }
    
  }
  return arr;
}


string *cUpdate::read_line_from_buffer(const char *buf, unsigned int *idx) {
  string s;

  if (buf == NULL) return NULL;

  while ((buf[*idx] != 0)&&(buf[*idx] != '\n')) {
    if (!iscntrl(buf[*idx])) s.append(1, buf[*idx]);
    (*idx)++;
  }
  while ( (buf[*idx] != 0) && (iscntrl(buf[*idx]) )) (*idx)++;
  return new string(s);
}

void cUpdate::ProcessImportedFile(const char *sBuffer)
{
    char **fields=NULL;
    char **tvtvjob=NULL;
    int field_cnt=0;
    unsigned int p=0;
    unsigned int i=0;
    bool vps = false;
    int iSID=0;
    bool timer_update=false;
#if VDRVERSNUM < 10336
    unsigned int max_desc_len=0;
#endif

    time_t tStartTime, tVpsTime, tEndTime;
    struct tm tStart, tVps, tEnd;

    int  StartTZ_h, StartTZ_m;
    char StartTZ_s;
    int  VpsTZ_h, VpsTZ_m;
    char VpsTZ_s;
    int  EndTZ_h, EndTZ_m;
    char EndTZ_s;
		
    cTimer *oTimer = NULL;
    cTimer *ti = NULL;
    cChannel *oTVTVChannel = NULL;

    string *sLine=NULL;
    string *sField=NULL;
  
    string sUID;
    string sChannel;
    string sStartTime;
    string sVpsTime;
    string sEndTime;
    string sTitle;
    string sDescription;
    string sPersons;
    string sNature;
    string sFormat;
    string sFSK;
    string sAction;
    string s;

    const string sTokenTvtvUid = "TVTV-UID: ";
    
    dsyslog("TVTV: Processing CSV data");

    // skip header
    do {
      sLine = read_line_from_buffer(sBuffer, &p);
      field_cnt = calc_field_cnt(sLine);
    } while (( field_cnt < 10 ) && ( !sLine->empty() ));
    fields=split_csv(sLine->c_str(), field_cnt);
    dsyslog("TVTV: Found %d fields in CSV data", field_cnt);

    // read timer jobs and create timers
    dsyslog("TVTV: Start reading timer jobs");
    sLine = read_line_from_buffer(sBuffer, &p);
    while (!sLine->empty()) {
      dsyslog("TVTV: Received '%s...'", sLine->substr(0,80).c_str());
      tvtvjob=split_csv(sLine->c_str(), field_cnt);
      if (tvtvjob != NULL) {
        for (int i=0; i<field_cnt; i++) {
  	  sField = new string(fields[i]);
  	  if (*sField == DEF_TVTV_SCHEDULE_UID)         { sUID = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_CHN)  { sChannel = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_STM)  { sStartTime = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_ETM)  { sEndTime = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_VTM)  { sVpsTime = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_TIT)  { sTitle = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_DESC) { sDescription = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_PERS) { sPersons = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_NAT)  { sNature = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_FRM)  { sFormat = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_FSK)  { sFSK = tvtvjob[i];
	  } else if (*sField == DEF_TVTV_SCHEDULE_ACT)  { sAction = tvtvjob[i];
 	  }
	  delete sField;
	  free(tvtvjob[i]);
        }
        free(tvtvjob);
      }

      // replace newline character by '|'
      i = sDescription.find('\n',0 );
      while (i != string::npos) { sDescription.replace(i, 1, 1, '|'); i = sDescription.find('\n',0 ); }
      i = sPersons.find('\n',0 );
      while (i != string::npos) { sPersons.replace(i, 1, 1, '|'); i = sPersons.find('\n',0 ); }
     

      iSID = FindChannelSID(sChannel.c_str());
      if (iSID > 0) {

	oTVTVChannel = GetChannelByServiceID(iSID);
	if (oTVTVChannel) {

	  oTimer = new cTimer;
			
	  // Calc Start and Stop Time
	  sscanf(sStartTime.c_str(), "%04d-%02d-%02d %02d:%02d:%02d %c%02d%02d",
		 &tStart.tm_year, &tStart.tm_mon, &tStart.tm_mday,
		 &tStart.tm_hour, &tStart.tm_min, &tStart.tm_sec,
		 &StartTZ_s, &StartTZ_h, &StartTZ_m);
	      
	  sscanf(sVpsTime.c_str(), "%04d-%02d-%02d %02d:%02d:%02d %c%02d%02d",
		 &tVps.tm_year, &tVps.tm_mon, &tVps.tm_mday,
		 &tVps.tm_hour, &tVps.tm_min, &tVps.tm_sec,
		 &VpsTZ_s, &VpsTZ_h, &VpsTZ_m);

	  sscanf(sEndTime.c_str(), "%04d-%02d-%02d %02d:%02d:%02d %c%02d%02d",
		 &tEnd.tm_year, &tEnd.tm_mon, &tEnd.tm_mday,
		 &tEnd.tm_hour, &tEnd.tm_min, &tEnd.tm_sec,
		 &EndTZ_s, &EndTZ_h, &EndTZ_m);
		  
	  tStart.tm_year -= 1900;
	  tStart.tm_mon  -= 1;
	  tStart.tm_isdst = -1;
	    
	  tVps.tm_year -= 1900;
	  tVps.tm_mon  -= 1;
	  tVps.tm_isdst = -1;
			
	  tEnd.tm_year -= 1900;
	  tEnd.tm_mon  -= 1;
	  tEnd.tm_isdst = -1;
			
	  tStartTime = timegm(&tStart);
	  tVpsTime   = timegm(&tVps);
	  tEndTime   = timegm(&tEnd);

// VPS was introduced with VDR 1.3.5
#if VDRVERSNUM >= 10305
	  vps = Setup.UseVps && (tVpsTime != -1) && TVTVConfig.usevps;
#else
	  vps = false;
#endif	        

	  tStartTime -= (StartTZ_h*60 + StartTZ_m)*60*((StartTZ_s == '-')?(-1):(1));
	  tEndTime   -= (EndTZ_h*60 + EndTZ_m)*60*((EndTZ_s == '-')?(-1):(1));
	  if (vps) tVpsTime -= (VpsTZ_h*60 + VpsTZ_m)*60*((VpsTZ_s == '-')?(-1):(1));
			
	  tStartTime -= Setup.MarginStart * 60;
	  if (!vps) tEndTime += Setup.MarginStop * 60;
	  localtime_r(&tStartTime, &tStart);
	  localtime_r(&tVpsTime, &tVps);
	  localtime_r(&tEndTime, &tEnd);
	
	  // Adding Genre and Station Name to File Name to make it more unique
	  if (TVTVConfig.usegenre && ! sNature.empty())
	    s = sNature + "~";
	  else
	    s = "";

	  if (TVTVConfig.usestation) 
#if VDRVERSNUM >= 10315
            s = s + oTVTVChannel->ShortName(true) + "~";
#else
	    s = s + oTVTVChannel->Name() + "~";
#endif
          s += sTitle;
	  
	  
	  // Colons should be replaced by '|' in title
	  i = s.find(':',0 ); 
	  while (i != string::npos) { s.replace(i, 1, 1, '|'); i = s.find(':',0 ); }
	  
	  // create timer string
	  ostringstream oss(ostringstream::out);
	  oss.fill('0');
	  oss << (tfActive|(vps?tfVps:tfNone)) << ":";
	  oss << oTVTVChannel->GetChannelID().ToString() << ":";

// Starting with VDR 1.3.23 timer entries support a full date in ISO notation. 
#if VDRVERSNUM >= 10323
          oss.width(4); oss << ((vps?(tVps.tm_year):(tStart.tm_year))+1900) << "-";
	  oss.width(2); oss << ((vps?(tVps.tm_mon):(tStart.tm_mon))+1) << "-";
#endif
          oss.width(2); oss << (vps?(tVps.tm_mday):(tStart.tm_mday)) << ":";
	  oss.width(4); oss << (vps?(tVps.tm_hour * 100 + tVps.tm_min):(tStart.tm_hour * 100 + tStart.tm_min)) << ":";
	  oss.width(4); oss << tEnd.tm_hour * 100 + tEnd.tm_min << ":";
	  oss << Setup.DefaultPriority << ":";
	  oss << Setup.DefaultLifetime << ":";
	  oss << s.c_str() << ":";
	  
	  dsyslog("TVTV: Timer entry '%s'", oss.str().c_str());
	  s=oss.str();

// Starting with VDR 1.3.44 the description within info.vdr is taken
// just from EPG. A description coming with a timer is added as a comment
// to info.vdr. This setup parameter decides whether to add this description
// as a comment or not.
#if VDRVERSNUM >= 10344
	  if (TVTVConfig.usetvtvdescr) {
#endif
	  
// Recording summary changed in VDR 1.3.25
#if VDRVERSNUM < 10325
	    s += sTitle;
	    if ((sPersons.length() > 0)||(sDescription.length() > 0)) s += "||";
#endif

	    if (sPersons.length() > 0) {
	      s += "Darsteller: " + sPersons;
  	      if (sDescription.length() > 0) s += "||" + sDescription;
	    } else {
	      if (sDescription.length() > 0) s += sDescription;
	    }

// Length of timer entry is limited to less than 10240 characters. Keeping off another
// 15 to avoid problems with additional characters (like newline). 
// This is not needed for VDR 1.3.36+ anymore!
#if VDRVERSNUM < 10336
	    max_desc_len = MAXPARSEBUFFER - sTokenTvtvUid.length() - sUID.length() - 15;
	    if (s.length() > max_desc_len) {
	      esyslog("TVTV: received description too long, cutted (%d -> %d)", s.length(), max_desc_len);
	      s.erase(max_desc_len);
	    }
#endif

#if VDRVERSNUM >= 10344
	  }
#endif
	  // Add Timer's UID to description to identify it later on
	  if (sUID.length() > 0) s += "||" + sTokenTvtvUid + sUID;

	  
          if (oTimer->Parse(s.c_str())) {
	    cTimer *t = Timers.GetTimer(oTimer);
	    if (t) { // Timer exists
	      delete oTimer;
	      if (sAction == "delete") {
		if (t->Recording()) {
		  isyslog("TVTV: timer %d recording, will be deleted after recording has ended (%s)", t->Index() + 1, t->File());
		} else {
		  isyslog("TVTV: timer %d deleted [%s/%s/%d/%04d-%04d]", oTimer->Index() + 1, sUID.c_str(), sChannel.c_str(), vps ? (tVps.tm_mday):(tStart.tm_mday), vps ? (tVps.tm_hour * 100 + tVps.tm_min):(tStart.tm_hour * 100 + tStart.tm_min), tEnd.tm_hour * 100 + tEnd.tm_min);
		  Timers.Del(t);
		}
	      } else {
		isyslog("TVTV: timer %d exist (%s)", t->Index() + 1, t->File());
	      }
	    } else { // if (t)

	      // try to figure out if timer needs to be updated
	      timer_update=false;
	      ti = Timers.First(); 
	      while (ti != NULL) { 
#if VDRVERSNUM >= 10344
		if (ti->Aux() != NULL) {
		  s=ti->Aux();
#else
                if (ti->Summary() != NULL) {
	          s=ti->Summary();
#endif
		  if (s.find(sTokenTvtvUid + sUID, 0) != string::npos) {
		    timer_update=true;
		    break;
		  } else {
		    ti = Timers.Next(ti); 
		  }
		} else {
		  ti = Timers.Next(ti);
		}
	      }

	      if (timer_update) { // Timer exists
		if (sAction == "rec") {
		  if (ti->Recording()) {
		    isyslog("TVTV: timer %d recording, will be deleted after recording has ended (%s)", ti->Index() + 1, ti->File());
		  } else {
		    Timers.Del(ti);
		  }
		  Timers.Add(oTimer);
		  isyslog("TVTV: timer %d updated (%s) [%s/%s/%d/%04d-%04d]", oTimer->Index() + 1, oTimer->File(), sUID.c_str(), sChannel.c_str(), vps ? (tVps.tm_mday):(tStart.tm_mday), vps ? (tVps.tm_hour * 100 + tVps.tm_min):(tStart.tm_hour * 100 + tStart.tm_min), tEnd.tm_hour * 100 + tEnd.tm_min);
		} else if (sAction == "delete") {
		  if (ti->Recording()) {
		    isyslog("TVTV: timer %d recording, will be deleted after recording has ended (%s)", ti->Index() + 1, ti->File());
		  } else {
		    isyslog("TVTV: timer %d deleted [%s/%s/%d/%04d-%04d]", oTimer->Index() + 1, sUID.c_str(), sChannel.c_str(), vps ? (tVps.tm_mday):(tStart.tm_mday), vps ? (tVps.tm_hour * 100 + tVps.tm_min):(tStart.tm_hour * 100 + tStart.tm_min), tEnd.tm_hour * 100 + tEnd.tm_min);
		    Timers.Del(ti);
		  }
		} 
	      } else { // if (timer_update)
		if (sAction == "rec") {
		  Timers.Add(oTimer);
		  isyslog("TVTV: timer %d added (%s) [%s/%s/%d/%04d-%04d]", oTimer->Index() + 1, oTimer->File(), sUID.c_str(), sChannel.c_str(), vps ? (tVps.tm_mday):(tStart.tm_mday), vps ? (tVps.tm_hour * 100 + tVps.tm_min):(tStart.tm_hour * 100 + tStart.tm_min), tEnd.tm_hour * 100 + tEnd.tm_min);
		}
	      }
		  
	    }
	  } else {
	    isyslog("TVTV: Timer Error: %s", s.c_str());
	  }

	} else { // if (oTVTVChannel)
	  isyslog("TVTV: SID <%d> for Channel <%s> was not found in channels.conf!", iSID, sChannel.c_str());
	}

      } else { // if (iSID > 0)
	isyslog("TVTV: Channel <%s> is not found!", sChannel.c_str());
      }

      // read next line
      sLine = read_line_from_buffer(sBuffer, &p);

    } // while (!sLine->empty())

    Timers.Save();
}

