/* ======================================================================== *\
!
! *
! * This file is part of MARS, the MAGIC Analysis and Reconstruction
! * Software. It is distributed to you in the hope that it can be a useful
! * and timesaving tool in analysing Data of imaging Cerenkov telescopes.
! * It is distributed WITHOUT ANY WARRANTY.
! *
! * Permission to use, copy, modify and distribute this software and its
! * documentation for any purpose is hereby granted without fee,
! * provided that the above copyright notice appear in all copies and
! * that both that copyright notice and this permission notice appear
! * in supporting documentation. It is provided "as is" without express
! * or implied warranty.
! *
!
!
!   Author(s): Thomas Bretz 1/2008 <mailto:tbretz@astro.uni-wuerzburg.de>
!
!   Copyright: MAGIC Software Development, 2000-2008
!
!
\* ======================================================================== */

/////////////////////////////////////////////////////////////////////////////
//
// MVideo
//
// Interface to Video4Linux at a simple level
//
/////////////////////////////////////////////////////////////////////////////
#include "MVideo.h"

// iostream
#include <iostream>

// open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <unistd.h>    // usleep
#include <errno.h>     // errno
#include <sys/mman.h>  // mmap
#include <sys/ioctl.h> // ioctl

#include <TEnv.h>
#include <TString.h>

#include "MLog.h"
#include "MLogManip.h"

#undef DEBUG

using namespace std;

//ClassImp(MVideo);

MVideoCtrl::MVideoCtrl(const v4l2_queryctrl &ctrl)
{
    fId      = ctrl.id;
    fName    = (const char*)ctrl.name;
    fMinimum = ctrl.minimum;
    fMaximum = ctrl.maximum;
    fStep    = ctrl.step;
    fDefault = ctrl.default_value;
}

// -------------------------------------------------------------
//
// Constructor. Specify the device (e.g. "/dev/video") to be used
//
MVideo::MVideo(const char *path) : fPath(path), fFileDesc(-1), fMapBuffer(0)
{
    Reset();

    fControls.SetOwner();
}

// -------------------------------------------------------------
//
// Internal function to reset the descriptors of the device
//
void MVideo::Reset()
{
    memset(&fCaps,    0, sizeof(fCaps));
    memset(&fChannel, 0, sizeof(fChannel));
    memset(&fBuffer,  0, sizeof(fBuffer));
    memset(&fAbil,    0, sizeof(fAbil));

    fFileDesc = -1;
    fMapBuffer = 0;
    fChannel.channel = -1;
    fAbil.tuner = -1;

    fControls.Delete();
}

// -------------------------------------------------------------
//
// Mapper around ioctl for easier access to the device
//
int MVideo::Ioctl(int req, void *opt, bool allowirq, bool force) const
{
    if (fFileDesc<0)
    {
        gLog << err << "ERROR - Ioctl: Device " << fPath << " not open." << endl;
        return -1;
    }

    while (1)
    {
        // FIXME: This call is a possible source for a hangup
        const int rc = ioctl(fFileDesc, req, opt);
        if (rc>=0)
            return rc;

        // errno== 4: Interrupted system call (e.g. by alarm())
        // errno==16: Device or resource busy
        if (errno==4 || errno==16)
        {
            if (!allowirq && errno==4)
                return -4;

            gLog << err << "ERROR - MVideo::Ioctl 0x" << hex << req << ": errno=" << dec << errno << " - ";
            gLog << strerror(errno) << " (rc=" << rc << ")" << endl;
            usleep(10);
            continue;
        }

        if (!force)
        {
            gLog << err << "ERROR - MVideo::Ioctl 0x" << hex << req << ": errno=" << dec << errno << " - ";
            gLog << strerror(errno) << " (rc=" << rc << ")" << endl;
        }
        return rc;
    }
    return -1;
}

// -------------------------------------------------------------
//
// Read the capabilities of the device
//
Bool_t MVideo::GetCapabilities()
{
    return Ioctl(VIDIOCGCAP, &fCaps)!=-1;
}

// -------------------------------------------------------------
//
// Read the properties of the device
//
Bool_t MVideo::GetProperties()
{
    return Ioctl(VIDIOCGPICT, &fProp)!=-1;
}

// -------------------------------------------------------------
//
// Read the video standard
//
Bool_t MVideo::GetVideoStandard()
{
    return Ioctl(VIDIOC_G_STD, &fVideoStandard)==-1;
}

// -------------------------------------------------------------
//
// Read the abilities of the tuner
//
Bool_t MVideo::GetTunerAbilities()
{
    fAbil.tuner = 0; // FIXME?
    return Ioctl(VIDIOCGTUNER, &fAbil)!=-1;
}

// -------------------------------------------------------------
//
// Enumerate (get) all controls from the device and store them
// as MVideoCtrl in fControls, starting with the id given as
// argument.
//
Bool_t MVideo::EnumerateControls(UInt_t id)
{
    struct v4l2_queryctrl qctrl;
    memset(&qctrl, 0, sizeof(qctrl));
    qctrl.id = id;

    while (1)
    {
        if (Ioctl(VIDIOC_QUERYCTRL, &qctrl, true, true)==-1)
            break;

        if (qctrl.maximum<=qctrl.minimum)
            continue;

        fControls.Add(new MVideoCtrl(qctrl));

        qctrl.id++;
    }

    return kTRUE;
}

// -------------------------------------------------------------
//
// Enumerate (get) all basic and private controls from the
// device and store them as MVideoCtrl in fControls.
//
Bool_t MVideo::EnumerateControls()
{
    if (!EnumerateControls(V4L2_CID_BASE))
        return kFALSE;
    if (!EnumerateControls(V4L2_CID_PRIVATE_BASE))
        return kFALSE;

    return kTRUE;
}

// -------------------------------------------------------------
//
// Reset a given control to it's default value as defined
// by the device.
//
Bool_t MVideo::ResetControl(MVideoCtrl &vctrl) const
{
    return WriteControl(vctrl, vctrl.fDefault);
}

// -------------------------------------------------------------
//
// Reset all enumereated device controls to their default.
// The default is defined by the device iteself.
//
Bool_t MVideo::ResetControls() const
{
    Bool_t rc = kTRUE;

    TIter Next(&fControls);
    MVideoCtrl *ctrl = 0;
    while ((ctrl=((MVideoCtrl*)Next())))
        if (!ResetControl(*ctrl))
        {
            gLog << err << "ERROR - Could not reset " << ctrl->fName << "." << endl;
            rc = kFALSE;
        }

    return rc;
}

// -------------------------------------------------------------
//
//  Read the value of the given control from the device
// and store it back into the given MVideoCtrl.
//
Bool_t MVideo::ReadControl(MVideoCtrl &vctrl) const
{
    struct v4l2_control ctrl = { vctrl.fId, 0 };
    if (Ioctl(VIDIOC_G_CTRL, &ctrl)==-1)
        return kFALSE;

    vctrl.fValue = ctrl.value;

    return kTRUE;
}

// -------------------------------------------------------------
//
// Write the given value into the given control of the device.
// On success the value is stored in the given MVideoCtrl.
//
Bool_t MVideo::WriteControl(MVideoCtrl &vctrl, Int_t val) const
{
    if (val<vctrl.fMinimum)
    {
        gLog << err << "ERROR - Value of " << val << " below minimum of " << vctrl.fMinimum << " for " << vctrl.fName << endl;
        return kFALSE;
    }

    if (val>vctrl.fMaximum)
    {
        gLog << err << "ERROR - Value of " << val << " above maximum of " << vctrl.fMaximum << " for " << vctrl.fName << endl;
        return kFALSE;
    }

    struct v4l2_control ctrl = { vctrl.fId, val };
    if (Ioctl(VIDIOC_S_CTRL, &ctrl)==-1)
        return kFALSE;

    vctrl.fValue = val;

    return kTRUE;
}

// -------------------------------------------------------------
//
// Set all controls from a TEnv. Note that all whitespaces
// and colons in the control names (as defined by the name of
// the MVideoCtrls stored in fControls) are replaced by
// underscores.
//
Bool_t MVideo::SetControls(TEnv &env) const
{
    Bool_t rc = kTRUE;

    TIter Next(&fControls);
    TObject *o = 0;
    while ((o=Next()))
    {
        if (!env.Defined(o->GetName()))
            continue;

        TString str = env.GetValue(o->GetName(), "");
        str = str.Strip(TString::kBoth);
        str.ReplaceAll(" ", "_");
        str.ReplaceAll(":", "_");
        if (str.IsNull())
            continue;

        MVideoCtrl &ctrl = *static_cast<MVideoCtrl*>(o);

        const Int_t val = str=="default" || str=="def" ?
            ctrl.fDefault : env.GetValue(o->GetName(), 0);

        if (!WriteControl(ctrl, val))
            rc = kFALSE;
    }

    return rc;
}


// -------------------------------------------------------------
//
// Open channel ch of the device
//
Bool_t MVideo::Open(Int_t ch)
{
    Bool_t rc = Init(ch);
    if (!rc)
        Close();
    return rc;
}

// -------------------------------------------------------------
//
// Open a channel of the device and retriev all necessary
// informations from the driver. Initialize the shared
// memory. Other access methods are not supported yet.
//
Bool_t MVideo::Init(Int_t channel)
{
    if (IsOpen())
    {
        gLog << warn << "WARNING - Device " << fPath << " already open." << endl;
        return kTRUE;
    }

    gLog << all << "Opening " << fPath << "... " << flush;
    do
    {
        fFileDesc = open(fPath, O_RDWR);
        usleep(1);
    }
    while (errno==19 && fFileDesc==-1);

    if (fFileDesc == -1)
    {
        gLog << err << "ERROR: " << strerror(errno) << endl;
        return kFALSE;
    }

    gLog << "done (" << fFileDesc << ")." << endl;

    // Close device on exit
    if (fcntl(fFileDesc, F_SETFD, FD_CLOEXEC)<0)
    {
        gLog << err << "ERROR - Call to fnctl (F_SETFD, FD_CLOEXEC) failed." << endl;
        return kFALSE;
    }

    gLog << warn << "Setting video standard to PAL-N." << endl;

    fVideoStandard = V4L2_STD_PAL_N;
    if (Ioctl(VIDIOC_S_STD, &fVideoStandard)==-1)
    {
        gLog << err << "ERROR - Could not set video standard to PAL-N." << endl;
        return kFALSE;
    }

    if (!EnumerateControls())
    {
        gLog << err << "ERROR - Could not enumerate controls." << endl;
        return kFALSE;
    }

    if (!ResetControls())
    {
        gLog << err << "ERROR - Could not reset controls to default." << endl;
        return kFALSE;
    }

    if (!GetCapabilities())
    {
        gLog << err << "ERROR - Getting device capabilities failed." << endl;
        return kFALSE;
    }

    if (!SetChannel(channel))
        return kFALSE;

    if (!GetProperties())
    {
        gLog << err << "ERROR - Couldn't get picture properties." << endl;
        return kFALSE;
    }
/*
    if (HasTuner())
    {
        if (!GetTunerAbilities())
        {
            gLog << err << "ERROR - Couldn't get tuner abilities." << endl;
            return kFALSE;
        }
    }
 */
    // get mmap grab buffer info
    if (Ioctl(VIDIOCGMBUF, &fBuffer)==-1)
    {
        gLog << err << "ERROR - Couldn't get info about memory map buffer." << endl;
        return kFALSE;
    }

    // map file (device) into memory
    fMapBuffer = (unsigned char*)mmap(0, fBuffer.size, PROT_READ|PROT_WRITE, MAP_SHARED, fFileDesc, 0);
    if (fMapBuffer == (void*)-1)
    {
        gLog << err << "ERROR - Couldn't map device buffer into memory." << endl;
        fMapBuffer = 0;
        return kFALSE;
    }

    Print();

    return kTRUE;
}

// -------------------------------------------------------------
//
// Close device. Free the shared memory
//
Int_t MVideo::Close()
{
    //    if (!IsOpen())
    //        return kTRUE;

    Bool_t rc = kTRUE;

    gLog << all << "Closing " << fPath << " (" << fFileDesc << ")... " << flush;
    if (fFileDesc != -1)
    {
        if (close(fFileDesc)<0)
        {
            gLog << err << "ERROR!" << endl;
            rc = kFALSE;
        }
        fFileDesc = -1;
    }
    gLog << "done." << endl;

    // unmap device memory
    if (fMapBuffer)
        munmap(fMapBuffer, fBuffer.size);

    Reset();

    return rc;
}

// -------------------------------------------------------------
//
// Instruct hardware to start capture into framebuffer frame
//
Bool_t MVideo::CaptureStart(unsigned int frame) const
{
    frame %= fBuffer.frames;

    struct video_mmap gb =
    {
        frame,                           // frame
        fCaps.maxheight, fCaps.maxwidth, // height, width
        VIDEO_PALETTE_RGB24             // palette
    };

#ifdef DEBUG
    gLog << dbg << "CapturStart(" << frame << ")" << endl;
#endif

    //
    // capture frame
    //
    if (Ioctl(VIDIOCMCAPTURE, &gb) != -1)
        return kTRUE;

//    if (errno == EAGAIN)
    gLog << err;
    gLog << "ERROR - Couldn't start capturing frame " << frame << "." << endl;
    gLog << "        Maybe your card doesn't support VIDEO_PALETTE_RGB24." << endl;

    return kFALSE;
}

// -------------------------------------------------------------
//
// Wait until hardware has finished capture into framebuffer frame
//
Int_t MVideo::CaptureWait(unsigned int frame, unsigned char **ptr) const
{
    frame %= fBuffer.frames;

    if (ptr)
        *ptr = NULL;

    const int SYNC_TIMEOUT = 1;

#ifdef DEBUG
    gLog << dbg << "CaptureWait(" << frame << ")" << endl;
#endif

    alarm(SYNC_TIMEOUT);
    const Int_t rc = Ioctl(VIDIOCSYNC, &frame, false);
    if (rc==-4)
    {
        //cout << "ERROR - Waiting for frame " << frame << " timed out." << endl;
        return kSKIP;
    }
    alarm(0);

    if (rc==-1)
    {
        gLog << err << "ERROR - Waiting for " << frame << " frame failed." << endl;
        return kFALSE;
    }

    if (ptr)
        *ptr = fMapBuffer+fBuffer.offsets[frame];

    return kTRUE;
}

// -------------------------------------------------------------
//
// Change the channel of a priviously opened device
//
Int_t MVideo::SetChannel(Int_t chan)
{
    if (fChannel.channel==chan)
        return kSKIP;

    if (chan<0 || chan>=fCaps.channels)
    {
        gLog << err << "ERROR - Set channel " << chan << " out of range." << endl;
        return kFALSE;
    }

    // Switch to channel
    struct video_channel ch = { chan, "", 0, 0, 0, 0 };
    if (Ioctl(VIDIOCSCHAN, &ch)==-1)
    {
        gLog << err << "ERROR - Couldn't switch to channel " << chan << "." << endl;
        gLog << "        You might need a bttv version > 0.5.13" << endl;
        return kFALSE;
    }

    // Get information about channel
    if (Ioctl(VIDIOCGCHAN, &ch)==-1)
    {
        gLog << err << "ERROR - Getting information for channel " << chan << " failed." << endl;
        return kFALSE;
    }

    memcpy(&fChannel, &ch, sizeof(fChannel));

    gLog << all << "Switched to channel " << chan << endl;

    return kTRUE;
}

// -------------------------------------------------------------
//
// Has the device capture capabilities?
//
Bool_t MVideo::CanCapture() const
{
    return fCaps.type&VID_TYPE_CAPTURE;
}

// -------------------------------------------------------------
//
// Has a tuner
//
Bool_t MVideo::HasTuner() const
{
    return fCaps.type&VID_TYPE_TUNER;
}

// -------------------------------------------------------------
//
// Returns the number of frame buffers which can be used
//
Int_t MVideo::GetNumBuffers() const
{
    return fBuffer.frames;
}

// -------------------------------------------------------------
//
// Maximum width of the frame which can be captured
//
Int_t MVideo::GetWidth() const
{
    return fCaps.maxwidth;
}

// -------------------------------------------------------------
//
// Maximum height of the frame which can be captured
//
Int_t MVideo::GetHeight() const
{
    return fCaps.maxheight;
}

// -------------------------------------------------------------
//
// Return the device type as string
//
TString MVideo::GetDevType(int type) const
{
    TString rc;
    if (CanCapture())
        rc += " capture";
    if (HasTuner())
        rc += " tuner";
    if (type&VID_TYPE_TELETEXT)
        rc += " teletext";
    if (type&VID_TYPE_OVERLAY)
        rc += " overlay";
    if (type&VID_TYPE_CHROMAKEY)
        rc += " chromakey";
    if (type&VID_TYPE_CLIPPING)
        rc += " clipping";
    if (type&VID_TYPE_FRAMERAM)
        rc += " frameram";
    if (type&VID_TYPE_SCALES)
        rc += " scales";
    if (type&VID_TYPE_MONOCHROME)
        rc += " monochrom";
    if (type&VID_TYPE_SUBCAPTURE)
        rc += " subcapature";
    return rc;
}

TString MVideo::GetTunerFlags(Int_t flags) const
{
    TString rc;
    if (flags&VIDEO_TUNER_PAL)
        rc += " PAL";
    if (flags&VIDEO_TUNER_NTSC)
        rc += " NTSC";
    if (flags&VIDEO_TUNER_SECAM)
        rc += " SECAM";
    if (flags&VIDEO_TUNER_LOW)
        rc += " kHz";
    if (flags&VIDEO_TUNER_NORM)
        rc += " CanSetNorm";
    if (flags&VIDEO_TUNER_STEREO_ON)
        rc += " StereoOn";
    return rc;
}

TString MVideo::GetTunerMode(Int_t mode) const
{
    switch (mode)
    {
    case VIDEO_MODE_PAL:
        return "PAL";
    case VIDEO_MODE_NTSC:
        return "NTSC";
    case VIDEO_MODE_SECAM:
        return "SECAM";
    case VIDEO_MODE_AUTO:
        return "AUTO";
    }
    return "undefined";
}

// -------------------------------------------------------------
//
// Return the channel flags as string
//
TString MVideo::GetChannelFlags(Int_t flags) const
{
    TString rc = "video";
    if (flags&VIDEO_VC_TUNER)
        rc += " tuner";
    if (flags&VIDEO_VC_AUDIO)
        rc += " audio";
//    if (flags&VIDEO_VC_NORM)
//        rc += " normsetting";
    return rc;
}

// -------------------------------------------------------------
//
// Return the channel type as string
//
TString MVideo::GetChannelType(Int_t type) const
{
    if (type&VIDEO_TYPE_TV)
        return "TV";
    if (type&VIDEO_TYPE_CAMERA)
        return "Camera";
    return "unknown";
}

// -------------------------------------------------------------
//
// Return the palette pal as string
//
TString MVideo::GetPalette(Int_t pal) const
{
    switch (pal)
    {
    case VIDEO_PALETTE_GREY:
        return "VIDEO_PALETTE_GREY: Linear intensity grey scale";
    case VIDEO_PALETTE_HI240:
        return "VIDEO_PALETTE_HI240: BT848 8-bit color cube";
    case VIDEO_PALETTE_RGB565:
        return "VIDEO_PALETTE_RGB565: RGB565 packed into 16-bit words";
    case VIDEO_PALETTE_RGB555:
        return "VIDEO_PALETTE_RGB555: RGB555 packed into 16-bit words, top bit undefined";
    case VIDEO_PALETTE_RGB24:
        return "VIDEO_PALETTE_RGB24: RGB888 packed into 24-bit words";
    case VIDEO_PALETTE_RGB32:
        return "VIDEO_PALETTE_RGB32: RGB888 packed into the low three bytes of 32-bit words. Top bits undefined.";
    case VIDEO_PALETTE_YUV422:
        return "VIDEO_PALETTE_YUV422: Video style YUV422 - 8-bit packed, 4-bit Y, 2-bits U, 2-bits V";
    case VIDEO_PALETTE_YUYV:
        return "VIDEO_PALETTE_YUYV: YUYV";
    case VIDEO_PALETTE_UYVY:
        return "VIDEO_PALETTE_UYVY: UYVY";
    case VIDEO_PALETTE_YUV420:
        return "VIDEO_PALETTE_YUV420: YUV420";
    case VIDEO_PALETTE_YUV411:
        return "VIDEO_PALETTE_YUV411: YUV411";
    case VIDEO_PALETTE_RAW:
        return "VIDEO_PALETTE_RAW: Raw capture (Bt848)";
    case VIDEO_PALETTE_YUV422P:
        return "VIDEO_PALETTE_YUV422P: YUV 4:2:2 planar";
    case VIDEO_PALETTE_YUV411P:
        return "VIDEO_PALETTE_YUV411P: YUV 4:1:1 planar";
    }
    return "unknown";
}

// -------------------------------------------------------------
//
// Print informations about the device, the capabilities, the
// channel and all available information
//
void MVideo::Print() const
{
    gLog << all << dec;

    gLog << "Device " << fPath << " " << (IsOpen()?"open":"closed") << "." << endl;

    if (!IsOpen())
        return;

    gLog  << " - Name:       " << fCaps.name << endl;
    gLog  << " - DevType:   "  << GetDevType(fCaps.type) << endl;
    gLog  << " - Channels:   " << fCaps.channels << endl;
    gLog  << " - Audios:     " << fCaps.audios << endl;
    gLog  << " - Size:       ";
    gLog  << fCaps.minwidth << "x" << fCaps.minheight << " to ";
    gLog  << fCaps.maxwidth << "x" << fCaps.maxheight << endl;
    gLog  << endl;
    if (fChannel.channel>=0)
    {
        gLog  << " - Channel:    " << fChannel.channel << " (" << fChannel.name << ")" << endl;
        gLog  << " - IsA:        " << GetChannelType(fChannel.type) << " with " << GetChannelFlags(fChannel.flags) << " (" << fChannel.flags << ")" << endl;
        //if (fChannel.flags&VIDEO_VC_NORM)
        gLog  << " - Norm:       " << fChannel.norm << endl;
        gLog  << endl;
    }

    if (fAbil.tuner>=0)
    {
        gLog << " - Tuner:           " << fAbil.tuner << endl;
        gLog << " - Name:            " << fAbil.name << endl;
        gLog << " - Tuner Range:     " << fAbil.rangelow << " - " << fAbil.rangehigh << endl;
        gLog << " - Tuner flags:    " << GetTunerFlags(fAbil.flags) << " (" << fAbil.flags << ")" << endl;
        gLog << " - Tuner mode:      " << GetTunerMode(fAbil.mode) << " (" << fAbil.mode << ")" <<endl;
        gLog << " - Signal Strength: " << fAbil.signal << endl;
    }

    gLog  << " - Brightness: " << fProp.brightness << endl;
    gLog  << " - Hue:        " << fProp.hue << endl;
    gLog  << " - Color:      " << fProp.colour << endl;
    gLog  << " - Contrast:   " << fProp.contrast << endl;
    gLog  << " - Whiteness:  " << fProp.whiteness << endl;
    gLog  << " - Depth:      " << fProp.depth << endl;
    gLog  << " - Palette:    " << GetPalette(fProp.palette) << " (" << fProp.palette << ")" << endl;
    gLog  << endl;

    gLog  << " - BufferSize: 0x" << hex << fBuffer.size << " (" << dec << fBuffer.frames << " frames)" << endl;
    gLog  << " - Offsets:   " << hex;
    for (int i=0; i<fBuffer.frames; i++)
        gLog  << " 0x" << fBuffer.offsets[i];
    gLog  << dec << endl;

    gLog << inf2 << "Controls:" << endl;
    fControls.Print();
}

/*
void MVideo::SetPicPar(int bright, int hue, int contrast)
{
    struct video_picture pict;

    Ioctl(VIDIOCGPICT, &pict);  // get

    if (contrast != -1)
        pict.contrast = contrast;

    if (bright != -1)
        pict.brightness = bright;

    if (hue != -1)
	pict.hue = hue;

    Ioctl(VIDIOCSPICT, &pict);  //set
}

void MVideo::GetPicPar(int *bright, int *hue, int *contrast)
{
    struct video_picture pict;

    Ioctl(VIDIOCGPICT, &pict);   // get

    *contrast = pict.contrast;
    *bright   = pict.brightness;
    *hue      = pict.hue;
}
*/
