/*
 * 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.10 2005/08/14 10:51:54 kls Exp $
 */

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


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

#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   50   // ms

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

cCuttingThread::cCuttingThread(const char *FromFileName, const char *ToFileName, cFileWriter *Writer)
:cThread("video cutting")
{
  writer=Writer;
  error = NULL;
  fromFile = toFile = -1;
  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)
{
  {
    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;

  cMark *Mark = fromMarks.First();
  if (Mark) {
     fromFile = fromFileName->Open();
     toFile = toFileName->Open();
     if (fromFile < 0 || toFile < 0)
        return;
     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 lIFrame = false;
     while (Running()) {
           uchar FileNumber;
           int FileOffset, Length;
           uchar PictureType;

           // Make sure there is enough disk space:

           AssertFreeDiskSpace(-1);

           // Read one frame:

		   if (writer && Index>=fromIndex->Last())
             continue;
           if (fromIndex->Get(Index++, &FileNumber, &FileOffset, &PictureType, &Length)) {
              if (Length==0)
              {
                Index++;
                continue;
              }
              if (FileNumber != CurrentFileNumber) {
                 fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
                 CurrentFileNumber = FileNumber;
                 }
              if (fromFile >= 0) {
                 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;
                 }
              }
           else
              break;

           // Write one frame:

           if (PictureType == I_FRAME) { // every file shall start with an I_FRAME
                 if (writer)
                   if (!lIFrame)
                   {
                     if (fromIndex->GetLast() - Index <= 1)
                     {
                       writer->RecordNextIFrame(((u_int64_t)(buffer[9]&0x0E)<<29)+((u_int64_t)buffer[10]<<22)+((u_int64_t)(buffer[11]&0xFE)<<14)+((u_int64_t)buffer[12]<<7)+((u_int64_t)(buffer[13]&0xFE)>>1));
                       lIFrame=true;
                     }
                   }
                   else
                     break;
              if (LastMark) // edited version shall end before next I-frame
                 break;
              if (FileSize > MEGABYTE(Setup.MaxVideoFileSize)) {
                 toFile = toFileName->NextFile();
                 if (toFile < 0) {
                    error = "toFile 1";
                    break;
                    }
                 FileSize = 0;
                 }
              LastIFrame = 0;

              if (cutIn) {
                 cRemux::SetBrokenLink(buffer, Length);
                 cutIn = false;
                 }
              }
#ifdef AVOID_TRASHING
           if (WriteStream(toFile, buffer, Length) < 0) {
#else
           if (safe_write(toFile, buffer, Length) < 0) {
#endif // AVOID_TRASHING
              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 < 0) {
                       error = "toFile 2";
                       break;
                       }
                    FileSize = 0;
                    }
                 }
              else
                 if (writer)
                   {
                     Index = fromIndex->GetNextIFrame(fromIndex->Last(),false);
                     CurrentFileNumber = 0;
                     cutIn = true;                    
                   }
                 else
                 LastMark = true;
              }

	   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) / 100;
	     //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;
	   }

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

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

char *cCutter::editedVersionName = NULL;
cCuttingThread *cCutter::cuttingThread = NULL;
bool cCutter::error = false;
bool cCutter::ended = false;
cLiveRecorder *cCutter::liveRecorder = NULL;
bool cCutter::Start(const char *FileName, cLiveRecorder *LiveRecorder, bool IsBeginning, cTimer *Timer)
{
  if (!cuttingThread) {
     liveRecorder=LiveRecorder;
     if (liveRecorder)
     liveRecorder->ChangePriority(100);
     error = false;
     ended = false;
     cRecording Recording(FileName);
     const char *evn = Recording.PrefixFileName('%',LiveRecorder != NULL);
     if (Timer && !IsBeginning)
     {
       cRecordControl *temp = new cRecordControl(NULL,Timer,false,false,true);
       evn = strdup(temp->FileName());
       delete temp;
     }
     if (IsBeginning)
       cRecordControls::Start(Timer,false,&evn);
     if (evn && (IsBeginning || 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);
        cuttingThread = new cCuttingThread(FileName, editedVersionName, IsBeginning ? cRecordControls::GetRecordControl(evn)->GetWriter() : NULL);
        return true;
        }
     }
  return false;
}

void cCutter::Stop(void)
{
  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);
     }
}

bool cCutter::Active(bool *needsLiveBuffer)
{
  if (cuttingThread) {
     if (cuttingThread->Active()) {
        if (needsLiveBuffer)
          *needsLiveBuffer = liveRecorder != NULL;
        return true;
        }
     error = cuttingThread->Error();
     Stop();
     if (!error)
        cRecordingUserCommand::InvokeCommand(RUC_EDITEDRECORDING, editedVersionName);
     free(editedVersionName);
     editedVersionName = NULL;
     ended = true;
     }
  return false;
}

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

bool cCutter::Ended(void)
{
  if (liveRecorder)
  {
    liveRecorder->ChangePriority(-1);
    liveRecorder=NULL;
  }
  bool result = ended;
  ended = false;
  return result;
}
