/************************************************************************
  			videodevice.h - Copyright micha

**************************************************************************/

#ifndef VIDEODEVICE_H
#define VIDEODEVICE_H

extern "C"
{
#include <unistd.h>
#include <fcntl.h>
#include <linux/videodev.h>
}

#include <opencv/cv.hpp>
#include <string>
#include <iostream>
#include "imagesource.h"
#include "../image.h"

using namespace std;

namespace seemicro
{

/**
  * Kapselung eines Dateihandles, um automatische Ressourcenfreigabe
  * garantieren zu können.  Siehe auch B. Stroustrup
  * "The C++ Programming Language" §14.4.
  */
class fileResource
{
  int f;

public:

  fileResource(const char *n, const int flags)
  {
    if((f = open(n, flags)) <= 0)
    {
      std::cerr << '"' << n << "\": "; perror("open");
      throw bad_alloc();
    }
  }

  fileResource(int ff) { f=ff; }

  ~fileResource() { if(f>=0) close(f); }

  operator int() { return f; }
};

/**
  * Kapselung eines Video4Linux1-Gerätes.
  * Aufgezeichnete Frames können aus \c img abgeholt werden.
  * Verarbeitet werden können die Kamerabildformate
  * \c VIDEO_PALETTE_GREY, \c VIDEO_PALETTE_RGB24 und
  * \c VIDEO_PALETTE_YUV420P.
  */
struct videoDevice : public ImageSource
{
    /** Gekapseltes Dateihandle zum Videogerät. */
    fileResource video_fd;

    /** V4L1-Datenstrukturen.
      * Unter Linux ist die Doku meist unter
      * /usr/src/linux/Documentation/video4linux/
      * zu finden.
      */
    struct video_capability capability;
    struct video_window win;
    struct video_channel vc;
    struct video_picture pict;
    struct video_mbuf vm;
    struct video_mmap mm;
    struct video_info vinfo;

    /**
      * Zeiger auf den Framebuffer des V4L-Gerätes, in dem
      * die Videodaten nach syncFrame() zur Verfügung stehen. */
    unsigned char *pict_buf;

    /** Format, in dem die Ausgabe des Videogerätes erfolgt. */
    int fmt;

    /**
      * Format, in dem die aufgenommenen Bilder in \c img zur Verfügung
      * gestellt werden. Ist der Wert, der dem Konstruktor
      * übergeben wurde.
      */
    int imgFmt;

    /**
      * Wenn die letzte VIDIOCMCAPTURE-Operation
      * erfolglos war, wird kein VIDIOSYNC ausgeführt. */
    int last_VIDIOCMCAPTURE_was_successful;

    /** Anzahl der bereits aufgenommenen Frames. */
    int framesCaptured;

    /** Anzahl der Frames, nach der ein Keyframe eingelegt werden soll. */
    int keyFrameRate;

    /**
      * Gibt an, ob wir im Verbiegemodus sind. In diesem Fall wird
      * wird in processFrame() \c img->imageData auf den Bereich in
      * pict_buf gesetzt, in dem der aktuell aufgenommene Frame
      * zu finden ist. Der Verbiegemodus ist also ein Zero-Copy-Modus.
      * Der Destruktor muß aufpassen, daß \c img->imageData vor
      * \c munmap() und \c cvDestroyImage(&img) auf \c imageDataOld
      * zurückgesetzt wird.
      */
    bool verbogen;

    /**
      * Gespeicherter Pointer von \c img->imageData, falls wir im
      * "Verbiegemodus" sind, also \c verbogen == true.
      */
    unsigned char *imageDataOld;


    /**
      * Öffnet und initialisiert das Gerät
      * @param device
      *	       Pfad zum zu verwendenden Gerät
      * @param imgFmt
      *	       Ausgabeformat, in dem \c img vorliegen soll
      *	       @arg \c VIDEO_PALETTE_GREY - img ist IPL_DEPTH_8U, 1 Ebene
      *	       @arg \c VIDEO_PALETTE_RGB24 - img ist IPL_DEPTH_8U, 3 Ebenen
      * @param channel
      *        einzustellender Kanal
      *	       @arg \c -1 nichts verstellen
      * @param reqWidth, reqHeight
      *        Größe, in der Frames aufgenommen werden sollen.
      *        Nächstkleinere vom Gerät akzeptierte Größe
      *	       wird verwendet. Diese kann mittels size()
      *        oder \c src->width/height ermittelt werden.
      *        Wird (0, 0) übergeben, wird die
      *        voreingestellte Bildgröße übernommen.
      */
  videoDevice(string device="/dev/video0", int imgFmt=VIDEO_PALETTE_GREY, int channel=-1,
    unsigned int reqWidth=0, unsigned int reqHeight=0);

  ~videoDevice();

  /**
    * Ruft VIDIOCSPICT auf. Die neuen Parameter außer \c fmt müssen schon in \c pict
    * eingetragen worden sein. Ist \c reqFmt == NULL, wird der
    * alte Wert übernommen.
    * @return Ergebnis des ioctl()-Aufrufs. Das letztendlich akzeptierte
    *	      Format wird gesetzt.
    */
  int setPicture(int *reqFmt, bool quiet=false);

  /**
    * Versucht das Bildformat fmt einzustellen.
    * @return true bei Erfolg, sonst false.
    */
  bool probeFmt(int fmt);

  /**
    * @return Wirklich verwendete Abmessungen.
    */
  CvSize* size();

  /**
    * Sendet Anfrage an Gerät, einen Frame im Userspace-Buffer zu speichern;
    * der kann später mit syncFrame() abgeholt werden.
    * Da das Gerät zwischen captureFrame() und syncFrame()
    * Zeit braucht, sollte die Verarbeitung des letzten Frames
    * in der Zwischenzeit erfolgen.
    * @return <0 bei Fehler, sonst 0.
    */
  int captureFrame(void);

  /**
    * Holt einen Frame vom Device in dieses Objekt, der zuvor mit
    * captureFrame() angefordert worden war.
    * @return <0 bei Fehler, sonst 0.
    */
  int syncFrame(void);

  /**
   * @return aktuelle Framerate in Frames/s.
   * Funkioniert nur, wenn diese
   * Funktion regelmäßig mit jedem Frame genau ein Mal aufgerufen wird.
   */
  float framerate(void);

  /**
    * Macht einen vollen Frame in \c img verfügbar.
    */
  void processKeyFrame(void)
  {
    processFrame();// ROI==NULL -> "alles"
  }

  void processFrame(myrect *AROI=NULL)
  {
    myrect ROI;
    if(AROI) ROI = *AROI;
    else ROI=myrect(0, 0, size()->width, size()->height);

    if(syncFrame())
    {
      cerr << "syncFrame(): error\n";
      return;
    }

    if(captureFrame())
    {
      cerr << "captureFrame(): error\n";
      // Groß rot ins Bild schreiben :)
      return;
    }

    // nun sind die Daten im Format fmt in pict_buf
    // sie sollen im Format imgFmt in img->imageData zur Verfügung stehen
    if(imgFmt==fmt==VIDEO_PALETTE_GREY || imgFmt==fmt==VIDEO_PALETTE_RGB24 ||
       (imgFmt==VIDEO_PALETTE_GREY && fmt==VIDEO_PALETTE_YUV420P))
    {
      // img->imageData auf pict_buf verbiegen
      // hoffentlich kommt kein Padding dazwischen

      // yuv420p2gray ist sehr einfach, da "y" der Grauwert ist und
      // die erste Plane am Anfang liegt
      // und "uv" nicht gebraucht werden - es liegt also schon
      // ein Graustufenbild vor

      if(!verbogen)
      {
        assert(img);
        imageDataOld = (unsigned char*)img->imageData;
	verbogen=true;
      }
      img->imageData = (char*)pict_buf + vm.offsets[mm.frame];
      return;
    }

    if(imgFmt==VIDEO_PALETTE_RGB24 && fmt==VIDEO_PALETTE_YUV420P)
    {
      // YUV420P 2 RGB 4 ROI
      // TODO: care for ROI
      yuv420p_unpack_2_rgb24(pict_buf + vm.offsets[mm.frame],
        img->imageData, win.width, win.height);
      return;
    }

    if(imgFmt==VIDEO_PALETTE_GREY && fmt==VIDEO_PALETTE_RGB24)
    {
      // RGB 2 GRAY 4 ROI
      __u32& w = win.width;
      //__u32& h = win.height;
      for(int y=0; y<ROI.r.height; y++)
      {
        int line = ROI.r.y + y;
        unsigned char *src = pict_buf + vm.offsets[mm.frame] + (line * w + ROI.r.x) * 3;
        unsigned char *dst = (unsigned char *)img->imageData + line * img->widthStep + ROI.r.x;
        for(int x=0;x<ROI.r.width; x++)
        {
          // pixel hernehmen
          // farbraum konvertieren
          // rgb2gray = (r+g+b)/3
          // into img->imgData speichern
          *dst++ = (src[0]+src[1]+src[2])/3;
          src += 3;
        }
      }
      return;
    }

    cerr << "Konvertierung nicht unterstützt! Ignoring.\n";
  }

}; // class

} // namespace

#endif // VIDEODEVICE_H

