/*
 * cal-cutter.c: A plugin for the Video Disk Recorder
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id$
 */

#include "cal-cutter.h"
#include "cal-config.h"
#include "cal-mpeg.h"
#include <vdr/recording.h>
#include <vdr/remux.h>
#include <vdr/skins.h>
#include <vdr/thread.h>
#include <vdr/videodir.h>

#define INFOFILESUFFIX "/info.vdr"

cString ExchangeRecordingName(const char *s, const char *NewRecordingName, int TimeOffset = 0) {
  const char *d = s + strlen(s);
  while( d > s && *d != '/' ) d--;
  if(d<=s) return NULL;
  const char *b = d-1;
  while( b > s && *b != '/' ) b--;
  if(b<=s) return NULL;
  
  char base[b-s+1];
  char oldRecordingName[d-b];
  char ext[s+strlen(s)-d];
  
  strncpy(base, s, b-s);
  base[b-s] = 0;
  
  strncpy(oldRecordingName, b+1, d-b);
  oldRecordingName[d-b-1] = 0;
  
  strncpy(ext, d+1, s+strlen(s)-d-1);
  ext[s+strlen(s)-d-1] = 0;
  
  char *efn = NULL;
  
  asprintf(&efn, "%s/%s/%s", base, (NewRecordingName ? NewRecordingName : oldRecordingName), ext);
  
  if (TimeOffset != 0) {
    cRecording Recording(efn);
    struct tm tm_r;
    time_t newTime = Recording.start + ((TimeOffset/FRAMESPERSEC+30)/60)*60;
    struct tm *t = localtime_r(&newTime, &tm_r);

    free(efn);
    asprintf(&efn, "%s/%s/%4d-%02d-%02d.%02d.%02d.%02d.%02d.rec", base, (NewRecordingName ? NewRecordingName : oldRecordingName), t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, Recording.priority, Recording.lifetime);
  }
  
  //fprintf(stderr, "%s\n", efn);
  
  return efn;
}

// --- cCalCuttingThread -----------------------------------------------------

class cCalCuttingThread : public cThread {
private:
  const char *error;
  cUnbufferedFile *fromFile, *toFile;
  cFileName *fromFileName, *toFileName;
  cIndexFile *fromIndex, *toIndex;
  cMarks fromMarks, toMarks;
  const char *fromFN;
  const char *currentFN;
  void SetupRecording(const char *FileName);
protected:
  virtual void Action(void);
public:
  cCalCuttingThread(const char *FromFileName, const char *ToFileName);
  virtual ~cCalCuttingThread();
  const char *Error(void) { return error; }
  };

void cCalCuttingThread::SetupRecording(const char *FileName) {
  if(FileName) {
    //fprintf(stderr, "Creating %s\n", FileName);
    cRecording Recording(fromFN);

    if(RemoveVideoFile(FileName) && MakeDirs(FileName, true)) {
      // XXX this can be removed once RenameVideoFile() follows symlinks (see videodir.c)
      // remove a possible deleted recording with the same name to avoid symlink mixups:
      char *s = strdup(FileName);
      char *e = strrchr(s, '.');
      if (e) {
        if (strcmp(e, ".rec") == 0) {
          strcpy(e, ".del");
          RemoveVideoFile(s);
        }
      }
      free(s);
      // XXX
    }

    const cRecordingInfo *info = Recording.Info();
    char *InfoFileName = NULL;
    asprintf(&InfoFileName, "%s%s", FileName, INFOFILESUFFIX);
    FILE *f = fopen(InfoFileName, "w");
    if(f) info->Write(f);
    fclose(f);
    f=NULL;
    free(InfoFileName);
    Recordings.AddByName(FileName);
    toFileName = new cFileName(FileName, true, true);
    toIndex = new cIndexFile(FileName, true);
    currentFN = strdup(FileName);
    toMarks.Load(FileName); // doesn't actually load marks, just sets the file name
  }
}

cCalCuttingThread::cCalCuttingThread(const char *FromFileName, const char *DefaultFileName)
:cThread("cut-a-lot Multi-cut")
{
  error = NULL;
  fromFile = toFile = NULL;
  fromFileName = toFileName = NULL;
  fromIndex = toIndex = NULL;
  fromFN = strdup(FromFileName);
  currentFN = NULL;
  const char *ToFileName;
  if (fromMarks.Load(FromFileName) && fromMarks.Count()) {
    cMark *Mark = fromMarks.First();
    if(Mark && Mark->comment) {
      if(calConfig.useCuttingPrefix==1) {
        ToFileName = strdup(PrefixVideoFileName(ExchangeRecordingName(fromFN, ExchangeChars(strdup(Mark->comment), true), (calConfig.adjustStartingTime==1 ? Mark->position : 0)), '%'));
      } else {
        ToFileName = strdup(ExchangeRecordingName(fromFN, ExchangeChars(strdup(Mark->comment), true), (calConfig.adjustStartingTime==1 ? Mark->position : 0)));
      }
    } else {
      if(calConfig.adjustStartingTime==1) {
        ToFileName = strdup(ExchangeRecordingName(fromFN, NULL, Mark->position));
      } else {
        // if no comment is set and there is no adjustment of starting time,
	// the recording name doesn't change. So let's add a % in order to not
	// destroy the original recording
        ToFileName = strdup(PrefixVideoFileName(fromFN, '%'));
      }
    }
    currentFN = strdup(ToFileName);
    fromFileName = new cFileName(fromFN, false, true);
    fromIndex = new cIndexFile(fromFN, false);
    SetupRecording(ToFileName);
    Start();
  } else
    esyslog("no editing marks found for %s", FromFileName);
}

cCalCuttingThread::~cCalCuttingThread()
{
  Cancel(3);
  delete fromFileName;
  if(toFileName) delete toFileName;
  delete fromIndex;
  if(toIndex) delete toIndex;
}

void cCalCuttingThread::Action(void)
{
  cMark *Mark = fromMarks.First();
  if (Mark) {
     fromFile = fromFileName->Open();
     toFile = toFileName->Open();
     if (!fromFile || !toFile)
        return;
     fromFile->SetReadAhead(MEGABYTE(20));
     int Index = Mark->position;
     Mark = fromMarks.Next(Mark);
     int FileSize = 0;
     int CurrentFileNumber = 0;
     int LastIFrame = 0;
     toMarks.Add(0);
     toMarks.Save();
     uchar buffer[MAXFRAMESIZE];
     bool LastMark = false;
     bool cutIn = true;
     bool newRecording = true;
     cCalPES *pes = NULL;
     int ptsDiff = 0, maxPtsDiff = 0;
     pts_t iFramePts = NO_PTS, newIFramePts = 0LL, newPts = NO_PTS;
     while (Running()) {
           uchar FileNumber;
           int FileOffset, Length;
           uchar PictureType;

           // Make sure there is enough disk space:

           AssertFreeDiskSpace(-1);

           // Read one frame:

           if (fromIndex->Get(Index++, &FileNumber, &FileOffset, &PictureType, &Length)) {
              if (FileNumber != CurrentFileNumber) {
                 fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
                 fromFile->SetReadAhead(MEGABYTE(20));
                 CurrentFileNumber = FileNumber;
                 }
              if (fromFile) {
                 int len = ReadFrame(fromFile, buffer,  Length, sizeof(buffer));
                 if (len < 0) {
                    error = "ReadFrame";
                    break;
                    }
                 if (len != Length) {
                    CurrentFileNumber = 0; // this re-syncs in case the frame was larger than the buffer
                    Length = len;
                    }
		 
		 if(calConfig.adjustPTS > 0) {
		   uchar *bpos = (uchar *)buffer;
		   int blen = len;
		 
		   while(blen > 0) {
                     pes = new cCalPES(bpos, blen);
                     if(pes) {
                       if(PictureType == I_FRAME && pes->GetStreamID() == 0xE0 && pes->HasPTS()) {
	                 iFramePts = pes->GetPTS();
	                 if(newRecording) newIFramePts = (calConfig.adjustPTS == 1 ? iFramePts : 0LL);
	                 newIFramePts = (newIFramePts + maxPtsDiff + (newRecording ? 0 : 3)*(90000 / FRAMESPERSEC)) % (MAX_PTS + 1);
	                 // TODO: Find out correct next I-Frame PTS by searching for the lowest PTS
	                 //       difference in the next GOP and add 90000 / FRAMESPERSEC to it
		         newRecording = false;
	                 maxPtsDiff = 0;
	               }
		   
		       if(pes->HasPTS()) {
		         ptsDiff = pes->GetPTS() - iFramePts;
	                 if(ptsDiff > maxPtsDiff) maxPtsDiff = ptsDiff;
	
	                 if(ptsDiff < 0 && newIFramePts < abs(ptsDiff)) {
	                   // means newIFramePts + ptsDiff < 0, must be so complicated because newIFramePts
	                   // is of type unsigned
	                   newPts = (newIFramePts + ptsDiff) + (MAX_PTS + 1);
	                 } else if (newIFramePts + ptsDiff > MAX_PTS) {
	                   newPts = (newIFramePts + ptsDiff) - (MAX_PTS + 1);
	                 } else {
	                   newPts = newIFramePts + ptsDiff;
	                 }
	           
		         pes->SetPTS(newPts);
	                 uchar *data = pes->GetData();
		         memcpy(bpos+9, data+9, 5);
		       }
		     }
		     bpos = bpos + pes->GetLength();
		     blen = blen - pes->GetLength();
		     delete pes;
		   }
		   }
                 }
              else {
                 error = "fromFile";
                 break;
                 }
              }
           else
              break;

           // Write one frame:

           if (PictureType == I_FRAME) { // every file shall start with an I_FRAME
              if (LastMark) // edited version shall end before next I-frame
                 break;
              if (FileSize > MEGABYTE(Setup.MaxVideoFileSize)) {
                 toFile = toFileName->NextFile();
                 if (!toFile) {
                    error = "toFile 1";
                    break;
                    }
                 FileSize = 0;
                 }
              LastIFrame = 0;

              if (cutIn) {
                 cRemux::SetBrokenLink(buffer, Length);
                 cutIn = false;
                 }
              }
           if (toFile->Write(buffer, Length) < 0) {
              error = "safe_write";
              break;
              }
           if (!toIndex->Write(PictureType, toFileName->Number(), FileSize)) {
              }
           FileSize += Length;
           if (!LastIFrame)
              LastIFrame = toIndex->Last();

           // Check editing marks:

           if (Mark && Index >= Mark->position) {
              Mark = fromMarks.Next(Mark);
              toMarks.Add(LastIFrame);
              if (Mark && !Mark->comment)
                 toMarks.Add(toIndex->Last() + 1);
              toMarks.Save();
              if (Mark) {
                 Index = Mark->position;
		 if(Mark->comment) {
		   newRecording = true;
		   delete toFileName;
		   delete toIndex;
		   cRecordingUserCommand::InvokeCommand("multi-cut", currentFN);
		   if(calConfig.useCuttingPrefix==1) {
		     SetupRecording(strdup(PrefixVideoFileName(ExchangeRecordingName(fromFN, ExchangeChars(strdup(Mark->comment), true), (calConfig.adjustStartingTime==1 ? Mark->position : 0)), '%')));
		   } else {
		     SetupRecording(strdup(ExchangeRecordingName(fromFN, ExchangeChars(strdup(Mark->comment), true), (calConfig.adjustStartingTime==1 ? Mark->position : 0))));
		   }
                   toFile = toFileName->Open();
		   if(!toFile) {
                     error = "next toFile";
                     break;
		   }
                   cMark *begMark = toMarks.Add(0);
		   begMark->comment = strdup(Mark->comment);
                   toMarks.Save();
                   FileSize = 0;
                 }
                 Mark = fromMarks.Next(Mark);
                 CurrentFileNumber = 0; // triggers SetOffset before reading next frame
                 cutIn = true;
              }
              else
                 LastMark = true;
              }
           }
    if(!error) {
      delete toFileName;
      toFileName = NULL;
      delete toIndex;
      toIndex = NULL;
      cRecordingUserCommand::InvokeCommand("multi-cut", currentFN);
    }
  } else
     esyslog("no editing marks found!");
}

// --- cCalCutter ------------------------------------------------------------

char *cCalCutter::editedVersionName = NULL;
cCalCuttingThread *cCalCutter::cuttingThread = NULL;
bool cCalCutter::error = false;
bool cCalCutter::ended = false;

bool cCalCutter::Start(const char *FileName) {
  if (!cuttingThread) {
    error = false;
    ended = false;
    cRecording Recording(FileName);
    const char *evn = Recording.PrefixFileName('%');
    editedVersionName = strdup(evn);
    //Recording.WriteInfo();
    //Recordings.AddByName(editedVersionName);
    cuttingThread = new cCalCuttingThread(FileName, editedVersionName);
    return true;
    // }
  }
  return false;
}

void cCalCutter::Stop(void) {
  bool Interrupted = cuttingThread && cuttingThread->Active();
  const char *Error = cuttingThread ? cuttingThread->Error() : NULL;
  delete cuttingThread;
  cuttingThread = NULL;
  if ((Interrupted || Error) && editedVersionName) { // get rid of editedVersionName?
    if (Interrupted)
      isyslog("Multi-cut process has been interrupted");
    if (Error)
      esyslog("ERROR: '%s' during Multi-cut process", Error);
    //RemoveVideoFile(editedVersionName); //XXX what if this file is currently being replayed?
    //Recordings.DelByName(editedVersionName);
  }
}

bool cCalCutter::Active(void) {
  if (cuttingThread) {
    if (cuttingThread->Active())
      return true;
    error = cuttingThread->Error();
    Stop();
    free(editedVersionName);
    editedVersionName = NULL;
    ended = true;
  }
  return false;
}

bool cCalCutter::Error(void) {
  bool result = error;
  error = false;
  return result;
}

bool cCalCutter::Ended(void) {
  bool result = ended;
  ended = false;
  return result;
}
