/*
 * 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.15 2006/02/12 10:07:23 kls Exp $
 */

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

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

class cCuttingThread : public cThread {
private:
  const char *error;
  cUnbufferedFile *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 = 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)
{
  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 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);
                 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;
                    }
                 }
              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) {
                    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)) {
              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
                 if (writer)
                   {
                     Index = fromIndex->GetNextIFrame(fromIndex->Last(),false);
                     CurrentFileNumber = 0;
                     cutIn = true;                    
                   }
                 else
                 LastMark = true;
              }
           }
     }
  else
     esyslog("no editing marks found!");
}

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

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

#define CUTTER_QUEUE
//#define CUTTER_AUTO_DELETE

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;
};

char *cCutter::editedVersionName = NULL;
cCuttingThread *cCutter::cuttingThread = NULL;
bool cCutter::error = false;
bool cCutter::ended = false;
cMutex *cCutter::cutterLock = new cMutex();

static uint64 /*cCutter::*/lastCuttingEndTime = 0;
static cList<cStringListObject> /**cCutter::*/cutterQueue /*= new cList<cStringListObject>*/;
cLiveRecorder *cCutter::liveRecorder = NULL;

bool cCutter::Start(const char *FileName, cLiveRecorder *LiveRecorder, bool IsBeginning, cTimer *Timer)
{
  cMutexLock(cutterLock);

#ifdef CUTTER_QUEUE
  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

  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)
{
  cMutexLock(cutterLock);
  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 CUTTER_QUEUE
     cutterQueue.Del(cutterQueue.First());
#endif
     }
#ifdef CUTTER_QUEUE
     lastCuttingEndTime = cTimeMs::Now();
#endif

}

bool cCutter::Active(bool *needsLiveBuffer)
{
  cMutexLock(cutterLock);
  
  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;
#ifdef CUTTER_AUTO_DELETE
     /* 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.Del(recording);
	 else
	   esyslog("ERROR: Can't delete '%s' after editing process", cutterQueue.First()->Value());
       }
     }
    lastCuttingEndTime = cTimeMs::Now();
#endif
#ifdef CUTTER_QUEUE
     cutterQueue.Del(cutterQueue.First());
#endif
     }
#ifdef CUTTER_QUEUE
  if(!cuttingThread && cutterQueue.First()) {
    /* start next cutting from queue*/
    if(cTimeMs::Now() > lastCuttingEndTime + WAIT_BEFORE_NEXT_CUT)
      Start(NULL);
  }
#endif

  return false;
}

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

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