/*
 * cutter.c: The video cutting facilities
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: cutter.c 1.18 2008/01/13 12:22:21 kls Exp $
 */

#include "cutter.h"
#include "recording.h"
#include "remux.h"
#include "thread.h"
#include "videodir.h"

// --- cCuttingThread --------------------------------------------------------

#ifdef USE_CUTTERLIMIT
#ifndef CUTTER_MAX_BANDWIDTH
#  define CUTTER_MAX_BANDWIDTH MEGABYTE(10) // 10 MB/s
#endif
#ifndef CUTTER_REL_BANDWIDTH
#  define CUTTER_REL_BANDWIDTH 75 // %
#endif
#ifndef CUTTER_PRIORITY
#  define CUTTER_PRIORITY sched_get_priority_min(SCHED_OTHER)
#endif
#define CUTTER_TIMESLICE   100   // ms
#endif /* CUTTERLIMIT */

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

cCuttingThread::cCuttingThread(const char *FromFileName, const char *ToFileName)
:cThread("video cutting")
{
  error = NULL;
  fromFile = toFile = NULL;
  fromFileName = toFileName = NULL;
  fromIndex = toIndex = NULL;
  if (fromMarks.Load(FromFileName) && fromMarks.Count()) {
     fromFileName = new cFileName(FromFileName, false, true);
     toFileName = new cFileName(ToFileName, true, true);
     fromIndex = new cIndexFile(FromFileName, false);
     toIndex = new cIndexFile(ToFileName, true);
     toMarks.Load(ToFileName); // doesn't actually load marks, just sets the file name
     Start();
     }
  else
     esyslog("no editing marks found for %s", FromFileName);
}

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

void cCuttingThread::Action(void)
{
#ifdef USE_CUTTERLIMIT
#ifdef USE_HARDLINKCUTTER
  if (!Setup.HardLinkCutter)
#endif /* HARDLINKCUTTER */
  {
    sched_param tmp;
    tmp.sched_priority = CUTTER_PRIORITY;
    if (!pthread_setschedparam(pthread_self(), SCHED_OTHER, &tmp))
       printf("cCuttingThread::Action: cant set priority\n");
  }

  int bytes = 0;
  int __attribute__((unused)) burst_size = CUTTER_MAX_BANDWIDTH * CUTTER_TIMESLICE / 1000; // max bytes/timeslice
  cTimeMs __attribute__((unused)) t;
#endif /* CUTTERLIMIT */

  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;
#ifdef USE_HARDLINKCUTTER
     bool SkipThisSourceFile = false;
#endif /* HARDLINKCUTTER */
     int LastIFrame = 0;
     toMarks.Add(0);
     toMarks.Save();
     uchar buffer[MAXFRAMESIZE];
     bool LastMark = false;
     bool cutIn = true;
     while (Running()) {
           uchar FileNumber;
           int FileOffset, Length;
           uchar PictureType;

           // Make sure there is enough disk space:

           AssertFreeDiskSpace(-1);

           // Read one frame:

#ifndef USE_HARDLINKCUTTER
           if (fromIndex->Get(Index++, &FileNumber, &FileOffset, &PictureType, &Length)) {
              if (FileNumber != CurrentFileNumber) {
                 fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
                 fromFile->SetReadAhead(MEGABYTE(20));
                 CurrentFileNumber = FileNumber;
                 }
#else
           if (!fromIndex->Get(Index++, &FileNumber, &FileOffset, &PictureType, &Length)) {
              error = "index";
              break;
              }

           if (FileNumber != CurrentFileNumber) {
              fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
              fromFile->SetReadAhead(MEGABYTE(20));
              CurrentFileNumber = FileNumber;
              if (SkipThisSourceFile) {
                 // At end of fast forward: Always skip to next file
                 toFile = toFileName->NextFile();
                 if (!toFile) {
                    error = "toFile 4";
                    break;
                    }
                 FileSize = 0;
                 SkipThisSourceFile = false;
                 }


              if (Setup.HardLinkCutter && FileOffset == 0) {
                 // We are at the beginning of a new source file.
                 // Do we need to copy the whole file?

                 // if !Mark && LastMark, then we're past the last cut-out and continue to next I-frame
                 // if !Mark && !LastMark, then there's just a cut-in, but no cut-out
                 // if Mark, then we're between a cut-in and a cut-out

                 uchar MarkFileNumber;
                 int MarkFileOffset;
                 // Get file number of next cut mark
                 if (!Mark && !LastMark
                     || Mark
                        && fromIndex->Get(Mark->position, &MarkFileNumber, &MarkFileOffset)
                        && (MarkFileNumber != CurrentFileNumber)) {
                    // The current source file will be copied completely.
                    // Start new output file unless we did that already
                    if (FileSize != 0) {
                       toFile = toFileName->NextFile();
                       if (!toFile) {
                          error = "toFile 3";
                          break;
                          }
                       FileSize = 0;
                       }

                    // Safety check that file has zero size
                    struct stat buf;
                    if (stat(toFileName->Name(), &buf) == 0) {
                       if (buf.st_size != 0) {
                          esyslog("cCuttingThread: File %s exists and has nonzero size", toFileName->Name());
                          error = "nonzero file exist";
                          break;
                          }
                       }
                    else if (errno != ENOENT) {
                       esyslog("cCuttingThread: stat failed on %s", toFileName->Name());
                       error = "stat";
                       break;
                       }

                    // Clean the existing 0-byte file
                    toFileName->Close();
                    cString ActualToFileName(ReadLink(toFileName->Name()), true);
                    unlink(ActualToFileName);
                    unlink(toFileName->Name());

                    // Try to create a hard link
                    if (HardLinkVideoFile(fromFileName->Name(), toFileName->Name())) {
                       // Success. Skip all data transfer for this file
                       SkipThisSourceFile = true;
                       cutIn = false;
                       toFile = NULL; // was deleted by toFileName->Close()
                       }
                    else {
                       // Fallback: Re-open the file if necessary
                       toFile = toFileName->Open();
                       }
                    }
                 } 
              }

           if (!SkipThisSourceFile) {
#endif /* HARDLINKCUTTER */
              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;
                    }
                 }
              else {
                 error = "fromFile";
                 break;
                 }
              }
#ifndef USE_HARDLINKCUTTER
           else {
              // Error, unless we're past the last cut-in and there's no cut-out
              if (Mark || LastMark)
                 error = "index";
              break;
              }

#endif /* HARDLINKCUTTER */
           // 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;
#ifndef USE_HARDLINKCUTTER
              if (FileSize > MEGABYTE(Setup.MaxVideoFileSize)) {
#else
              if (!SkipThisSourceFile && FileSize > toFileName->MaxFileSize()) {
#endif /* HARDLINKCUTTER */
                 toFile = toFileName->NextFile();
                 if (!toFile) {
                    error = "toFile 1";
                    break;
                    }
                 FileSize = 0;
                 }
              LastIFrame = 0;

#ifndef USE_HARDLINKCUTTER
              if (cutIn) {
#else
              if (!SkipThisSourceFile && cutIn) {
#endif /* HARDLINKCUTTER */
                 cRemux::SetBrokenLink(buffer, Length);
                 cutIn = false;
                 }
              }
#ifndef USE_HARDLINKCUTTER
           if (toFile->Write(buffer, Length) < 0) {
#else
           if (!SkipThisSourceFile && toFile->Write(buffer, Length) < 0) {
#endif /* HARDLINKCUTTER */
              error = "safe_write";
              break;
              }
           if (!toIndex->Write(PictureType, toFileName->Number(), FileSize)) {
              error = "toIndex";
              break;
              }
           FileSize += Length;
           if (!LastIFrame)
              LastIFrame = toIndex->Last();

           // Check editing marks:

           if (Mark && Index >= Mark->position) {
              Mark = fromMarks.Next(Mark);
              toMarks.Add(LastIFrame);
              if (Mark)
                 toMarks.Add(toIndex->Last() + 1);
              toMarks.Save();
              if (Mark) {
                 Index = Mark->position;
                 Mark = fromMarks.Next(Mark);
                 CurrentFileNumber = 0; // triggers SetOffset before reading next frame
                 cutIn = true;
                 if (Setup.SplitEditedFiles) {
                    toFile = toFileName->NextFile();
                    if (!toFile) {
                       error = "toFile 2";
                       break;
                       }
                    FileSize = 0;
                    }
                 }
              else
#ifndef USE_HARDLINKCUTTER
                 LastMark = true;
#else
                 LastMark = true; // After last cut-out: Write on until next I-frame, then exit
#endif /* HARDLINKCUTTER */
              }
#ifdef USE_CUTTERLIMIT
#ifdef USE_HARDLINKCUTTER
           if (!Setup.HardLinkCutter) {
#endif /* HARDLINKCUTTER */
           bytes += Length;
           if (bytes >= burst_size) {
              int elapsed = t.Elapsed();
              int sleep = 0;

#if CUTTER_REL_BANDWIDTH > 0 && CUTTER_REL_BANDWIDTH < 100
              // stay under max. relative bandwidth

              sleep = (elapsed * 100 / CUTTER_REL_BANDWIDTH) - elapsed;
              //if (sleep<=0 && elapsed<=2) sleep = 1; 
              //if (sleep) esyslog("cutter: relative bandwidth limit, sleep %d ms (chunk %dk / %dms)", sleep, burst_size/1024, CUTTER_TIMESLICE);
#endif
              // stay under max. absolute bandwidth
              if (elapsed < CUTTER_TIMESLICE) {
                 sleep = max(CUTTER_TIMESLICE - elapsed, sleep);
                 //if (sleep) esyslog("cutter: absolute bandwidth limit, sleep %d ms (chunk %dk / %dms)", sleep, burst_size/1024, CUTTER_TIMESLICE);
                 }

              if (sleep>0)
                 cCondWait::SleepMs(sleep);
                 t.Set();
                 bytes = 0;
              }
#ifdef USE_HARDLINKCUTTER
              }
#endif /* HARDLINKCUTTER */
#endif /* CUTTERLIMIT */

           }
     Recordings.TouchUpdate();
     }
  else
     esyslog("no editing marks found!");
}

// --- cCutter ---------------------------------------------------------------

#ifdef USE_CUTTERQUEUE
#define WAIT_BEFORE_NEXT_CUT   (10*1000)  // 10 seconds

class cStringListObject : public cListObject {
  public:
    cStringListObject(const char *s) { str = strdup(s); }
    ~cStringListObject() { free(str); }

    const char *Value() { return str; }
    operator const char * () { return str; }

  private:
    char *str;
};
#endif /* CUTTERQUEUE */

char *cCutter::editedVersionName = NULL;
cCuttingThread *cCutter::cuttingThread = NULL;
bool cCutter::error = false;
bool cCutter::ended = false;
#ifdef USE_CUTTERQUEUE
cMutex *cCutter::cutterLock = new cMutex();
static uint64_t /*cCutter::*/lastCuttingEndTime = 0;
static cList<cStringListObject> /**cCutter::*/cutterQueue /*= new cList<cStringListObject>*/;
#endif /* CUTTERQUEUE */

bool cCutter::Start(const char *FileName)
{
#ifdef USE_CUTTERQUEUE
  cMutexLock(cutterLock);
  if (FileName) {
     /* Add file to queue.
      * If cutter is still active, next cutting will be started 
      * when vdr.c:main calls cCutter::Active and previous cutting has 
      * been stopped > 10 s before 
      */
     cutterQueue.Add(new cStringListObject(FileName));
     }
  if (cuttingThread)
     return true;
  /* cut next file from queue */
  if (!(cutterQueue.First()))
     return false;
  FileName = cutterQueue.First()->Value();
#endif /* CUTTERQUEUE */
  if (!cuttingThread) {
     error = false;
     ended = false;
     cRecording Recording(FileName);
#ifdef USE_CUTTIME
     cMarks FromMarks;
     FromMarks.Load(FileName);
     cMark *First=FromMarks.First();
     if (First) Recording.SetStartTime(Recording.start+((First->position/FRAMESPERSEC+30)/60)*60);
#endif /* CUTTIME */
     const char *evn = Recording.PrefixFileName('%');
#ifdef USE_CUTTERQUEUE
     if (!(Recordings.GetByName(FileName))) {
        // Should _not_ remove any cutted recordings
        // (original recording already deleted ?)
        // so, just pop item from queue and return.
        esyslog("can't cut non-existing recording %s", FileName);
        cutterQueue.Del(cutterQueue.First());
        return true; // might be already queued recording
        }
#endif /* CUTTERQUEUE */
     if (evn && RemoveVideoFile(evn) && MakeDirs(evn, 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(evn);
        char *e = strrchr(s, '.');
        if (e) {
           if (strcmp(e, ".rec") == 0) {
              strcpy(e, ".del");
              RemoveVideoFile(s);
              }
           }
        free(s);
        // XXX
        editedVersionName = strdup(evn);
        Recording.WriteInfo();
        Recordings.AddByName(editedVersionName, false);
        cuttingThread = new cCuttingThread(FileName, editedVersionName);
        return true;
        }
     }
  return false;
}

void cCutter::Stop(void)
{
#ifdef USE_CUTTERQUEUE
  cMutexLock(cutterLock);
#endif /* CUTTERQUEUE */
  bool Interrupted = cuttingThread && cuttingThread->Active();
  const char *Error = cuttingThread ? cuttingThread->Error() : NULL;
  delete cuttingThread;
  cuttingThread = NULL;
  if ((Interrupted || Error) && editedVersionName) {
     if (Interrupted)
        isyslog("editing process has been interrupted");
     if (Error)
        esyslog("ERROR: '%s' during editing process", Error);
     RemoveVideoFile(editedVersionName); //XXX what if this file is currently being replayed?
     Recordings.DelByName(editedVersionName);
#ifdef USE_CUTTERQUEUE
     cutterQueue.Del(cutterQueue.First());
#endif /* CUTTERQUEUE */
     }
#ifdef USE_CUTTERQUEUE
  lastCuttingEndTime = cTimeMs::Now();
#endif /* CUTTERQUEUE */
}

bool cCutter::Active(void)
{
#ifdef USE_CUTTERQUEUE
  cMutexLock(cutterLock);
#endif /* CUTTERQUEUE */
  if (cuttingThread) {
     if (cuttingThread->Active())
        return true;
     error = cuttingThread->Error();
     Stop();
     if (!error)
        cRecordingUserCommand::InvokeCommand(RUC_EDITEDRECORDING, editedVersionName);
     free(editedVersionName);
     editedVersionName = NULL;
     ended = true;
#ifdef USE_CUTTERQUEUE
     if (Setup.CutterAutoDelete) {
        /* Remove original (if cutting was successful) */
        if (!error) {
           cRecording *recording = Recordings.GetByName(*cutterQueue.First());
           if (!recording)
              esyslog("ERROR: Can't found '%s' after editing process", cutterQueue.First()->Value());
           else {
              if (recording->Delete())
                 Recordings.DelByName(recording->FileName());
              else
                 esyslog("ERROR: Can't delete '%s' after editing process", cutterQueue.First()->Value());
              }
           }
        lastCuttingEndTime = cTimeMs::Now();
        }
     cutterQueue.Del(cutterQueue.First());
#endif /* CUTTERQUEUE */
     }
#ifdef USE_CUTTERQUEUE
  if (!cuttingThread && cutterQueue.First()) {
     /* start next cutting from queue*/
     if (cTimeMs::Now() > lastCuttingEndTime + WAIT_BEFORE_NEXT_CUT)
        Start(NULL);
     }
#endif /* CUTTERQUEUE */
  return false;
}

bool cCutter::Error(void)
{
#ifdef USE_CUTTERQUEUE
  cMutexLock(cutterLock);
#endif /* CUTTERQUEUE */
  bool result = error;
  error = false;
  return result;
}

bool cCutter::Ended(void)
{
#ifdef USE_CUTTERQUEUE
  cMutexLock(cutterLock);
#endif /* CUTTERQUEUE */
  bool result = ended;
  ended = false;
  return result;
}
