/* 
 * Copyright (C) 2002-2003 George Staikos <staikos@kde.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */


#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <endian.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/mman.h>
#include <assert.h>
#include <signal.h>

#include <qimage.h>
#include <kdebug.h>

#include "v4ldev.h"
#include "v4ldevtuner.h"
#include "v4ldevcamera.h"
#include "v4lutil.h"

#include <sys/time.h>


/* FIXME:
 *     - Overlay is broken.
 *     - Colourspaces are not done.
 */



static int _sigs = 0;

static void sigalarm(int) {
	_sigs++;
	kdDebug() << "V4L timeout " << _sigs << endl;
}

class V4LSigInit {
	public:
		V4LSigInit() {
			_old = signal(SIGALRM, sigalarm);
		}

		~V4LSigInit() {
			signal(SIGALRM, _old);
		}

		void (*_old)(int);
};

static V4LSigInit _initSignals;


V4LDev* V4LDev::getDevice( const QString &dev )
{
    int rc;
    struct video_capability vcap;
    int fd = ::open(dev.local8Bit(), O_RDWR);

    if (fd < 0)
	return NULL;

	/* get the capture properties */

    rc = ioctl(fd, VIDIOCGCAP, &vcap);
    if (rc < 0) {
	::close(fd);
	return NULL;
    }

    kdDebug() << "Grabber Name: " << vcap.name << endl;
    kdDebug() << "Type: " << vcap.type << endl;
    kdDebug() << "Input Channels: " << vcap.channels << endl;
    kdDebug() << "minw=" << vcap.minwidth << ", minh=" << vcap.minheight
	      << ", maxw=" << vcap.maxwidth << ", maxh=" << vcap.maxheight << endl;

    if (vcap.type&VID_TYPE_CAPTURE) {
	kdDebug() << "  Supports: capture to memory" << endl;
    }
    if (vcap.type&VID_TYPE_OVERLAY) {
	kdDebug() << "  Supports: video overlay" << endl;
    }
    if (vcap.type&VID_TYPE_CLIPPING) {
	kdDebug() << "  Supports: clipping" << endl;
    }
    if (vcap.type&VID_TYPE_CHROMAKEY) {
	kdDebug() << "  Requires: chromakey" << endl;
    }
    if (vcap.type&VID_TYPE_SCALES) {
	kdDebug() << "  Supports: scaling" << endl;
    }
    if (vcap.type&VID_TYPE_FRAMERAM) {
	kdDebug() << "  Requires: overwriting frame buffer" << endl;
    }
    if (vcap.type&VID_TYPE_SUBCAPTURE) {
	kdDebug() << "  Supports: capture of parts to memory" << endl;
    }

    V4LDev *nd = NULL;
    if (vcap.type & VID_TYPE_TUNER) {   /* it must be a TV tuner */
	nd = new V4LTuner(fd, vcap.name, vcap.channels, vcap.type,
			      vcap.minwidth, vcap.minheight,
			      vcap.maxwidth, vcap.maxheight);
    /* see if it's a camera */
    } else if (vcap.channels == 1) {  // hack for now   FIXME
        nd =  new V4LCamera(fd, vcap.name, vcap.channels, 
				vcap.type, vcap.minwidth, vcap.minheight,
				vcap.maxwidth, vcap.maxheight);
    } else {
	::close(fd);
    }

    return nd;
}


V4LDev::V4LDev(int fd, const QString &name, int channels, int type, int minw, int minh, int maxw, int maxh) 
    : _fd(fd), _name(name), _minWidth(minw), _minHeight(minh), _maxWidth(maxw), _maxHeight(maxh), _type(type) {
    int rc;

    _source = 0;
    _isTuner = false;
    _hasAudio = false;
    _aspectRatio = (float)maxw/(float)maxh;
    _overlaid = false;

    _fmt = VIDEO_PALETTE_YUV422;
    _mmapBuf = 0;
    _grabBuf = 0;
    _readBuf = 0;
    _mmapCurrentFrame = 0;
    _grabNeedsInit = true;
    _mmapData = 0;
    _grabW = maxw;
    _grabH = maxh;

    setImageSize(maxw, maxh);

    /* Get the channels */
    _channels = new video_channel[channels];
    memset(_channels, 0, sizeof(_channels));

    for (int i = 0; i < channels; i++) {
	_channels[i].channel = i;
	rc = ioctl(_fd, VIDIOCGCHAN, &_channels[i]);
	if (rc >= 0) {
	    _sources << QString(_channels[i].name);

            /**** DEBUG ****/
	    kdDebug() << "Channel " << i+1 << ": " << _channels[i].name << endl;

	    QString type;
	    if ( _channels[i].type & VIDEO_TYPE_TV )
		type = "TV";
	    else if ( _channels[i].type & VIDEO_TYPE_CAMERA )
		type = "Camera";
	    else
		type ="Unknown Type";

	    QString msg( "%1" );
	    if  ( _channels[i].flags & VIDEO_VC_TUNER )
		msg = msg + QString(" - tuner(%1) ").arg(_channels[i].tuners);
	    if ( _channels[i].flags & VIDEO_VC_AUDIO )
		msg = msg + " - audio ";

	    kdDebug() << msg.arg(type) << endl;
	    /**** END DEBUG ****/
	}
    }
}


V4LDev::~V4LDev()
{
    delete[] _channels;
    int zero = 0;

    if (_mmapBuf)
        munmap(_mmapBuf, _mbuf.size);

    delete[] _grabBuf;
    delete[] _readBuf;
    delete[] _mmapData;

    ioctl(_fd, VIDIOCCAPTURE, &zero);
    ::close(_fd);
}



int V4LDev::setSource(const QString &source) {
    kdDebug() << "V4LDev::setSource(..) Source is " << source << endl;
    int idx = _sources.findIndex(source);

    if (idx < 0) return -1;

    struct video_channel vc;

    memset(&vc, 0, sizeof(vc));
    vc.channel = idx;

    int rc = ioctl(_fd, VIDIOCGCHAN, &vc);
    if (rc < 0)
        return -1;

    rc = ioctl(_fd, VIDIOCSCHAN, &vc);
    if (rc < 0) {
	kdDebug() << "Error setting source to " << idx << endl;
    }

    _source = idx;

    return 0;
}


const QString V4LDev::source() const {
    return _sources[_source];
}



bool V4LDev::canGrab() const {
    if (_type&VID_TYPE_CAPTURE)
	return true;
    return false;
}


bool V4LDev::canOverlay() const {
    if (_type&VID_TYPE_OVERLAY)
	return true;
    return false;
}


int V4LDev::initGrabbing() {
        _grabNeedsInit = false;

        if (_mmapBuf) {
            munmap(_mmapBuf, _mbuf.size);
            _mmapBuf = 0;
        }

        if (_grabBuf) {
            delete[] _grabBuf;
            _grabBuf = 0;
        }

        if (_mmapData) {
            delete[] _mmapData;
            _mmapData = 0;
        }

        _mmapCurrentFrame = 0;

        _grabBuf = new uchar[_grabW*_grabH*4];

        if (_type & VID_TYPE_CAPTURE) {
            int rc = ioctl(_fd, VIDIOCGMBUF, &_mbuf);
            if (rc == 0) {
                void *rc2 = mmap(0, _mbuf.size, PROT_READ|PROT_WRITE, MAP_SHARED, _fd, 0);
                if ((int)rc2 != -1 && rc2 != 0) {
                    _mmapBuf = static_cast<uchar*>(rc2);
                } else {
                    kdDebug() << "MMAP error." << endl;
                    return -1;
                }
           } else return -1;

           _mmapData = new struct video_mmap[_mbuf.frames];
	   memset(_mmapData, 0, _mbuf.frames*sizeof(struct video_mmap));
           for (int i = 0; i < _mbuf.frames; i++) {
               _mmapData[i].width = _grabW;
               _mmapData[i].height = _grabH;
               _mmapData[i].frame = i;
               _mmapData[i].format = _fmt;
           }

           rc = ioctl(_fd, VIDIOCMCAPTURE, _mmapData);
           if (rc != 0) {
               kdDebug() << "VIDIOCMCAPTURE failed." << endl;
               return -1;
           }
        }
return 0;
}


int V4LDev::setInputFormat(int fmt) {
int ig;

	ig = V4LBppForFormat(fmt);
	if (ig < 0)
		return -1;

	_bpp = ig;
	_fmt = fmt;
	ig = initGrabbing();

	return ig;
}


int V4LDev::inputFormat() const {
	return _fmt;
}


int V4LDev::grab(V4LImage *img, bool scale) {
    if (_grabNeedsInit) {
        initGrabbing();
    }

    assert(_grabBuf);
    assert(!scale); // not working yet

    if (_mmapBuf) {  ////////////////////////////////////   mmap() method
	int rc;
        int nextFrame = _mmapCurrentFrame+1;
        if (nextFrame >= _mbuf.frames)
            nextFrame = 0;

        rc = ioctl(_fd, VIDIOCMCAPTURE, &(_mmapData[nextFrame]));
        if (rc != 0) {
            //perror("VIDIOCMCAPTURE");
            kdDebug() << "VIDIOCMCAPTURE failed." << endl;
            if (errno == EBUSY) {
                // on EBUSY, somehow the frame is busy.  This should never
		// happen as far as I know.  Hack: try to release the frame.
		// FIXME - try to find out why this happens.
		//         It can be triggered by resizing several times.
                rc = ioctl(_fd, VIDIOCSYNC, &nextFrame);
                if (rc != 0) {
                    //perror("VIDIOCSYNC");
                    kdDebug() << "VIDIOCSYNC failed too." << endl;
                }
            }
            return -1;
        }

	if (img) {
		if (img->owner) {
			img->owner = false;
			delete[] img->buffer;
		}

		img->bpp = _bpp;
		img->width = _grabW;
		img->height = _grabH;
		img->format = _fmt;

		img->buffer = _mmapBuf + _mbuf.offsets[_mmapCurrentFrame];
	}

        while ((rc = ioctl(_fd, VIDIOCSYNC, &_mmapCurrentFrame)) == -1
               && errno == EINTR)
            ;
        _mmapCurrentFrame = nextFrame;
        if (rc != 0) {
            kdDebug() << "VIDIOCSYNC failed." << endl;
        }
    } else {    /////////////////////////////////////////   read() method
// FIXME: there is no need to read into _readBuf and then memcpy(), other than
//        if img is null.  Let's save the memcpy!
        int sz = _grabW*_grabH*_bpp;
        if (!_readBuf)
            _readBuf = new uchar[sz];
        int rc = read(_fd, _readBuf, sz);
        if (rc != sz) {
            kdDebug() << "error: wanted " << sz
                      << ", got rc = " << rc << endl;

            return -1;
        }

	if (img) {
		if (img->width*img->height*img->bpp < sz || !img->owner) {
			if (img->owner) {
				delete[] img->buffer;
			} else {
				img->owner = true;
			}
			img->buffer = new uchar[sz];
		}

		img->bpp = _bpp;
		img->width = _grabW;
		img->height = _grabH;
		img->format = _fmt;

		memcpy(img->buffer, _readBuf, sz);
	}
    }

    return 0;
}



int V4LDev::setColourKey(unsigned long x) {
    struct video_window vw;

    int rc = ioctl(_fd, VIDIOCGWIN, &vw);
    if (rc < 0)
        return -1;
    vw.chromakey = x;
    vw.flags = 0;
    if (_type&VID_TYPE_CHROMAKEY) {
        kdDebug() << "Enabling chromakey for V4L overlay." << endl;
        vw.flags |= VIDEO_WINDOW_CHROMAKEY;
    }
    return ioctl(_fd, VIDIOCSWIN, &vw);
}


unsigned long V4LDev::colourKey() const {
    struct video_window vw;

    int rc = ioctl(_fd, VIDIOCGWIN, &vw);
    if (rc < 0)
        return 0;

    return vw.chromakey;
}


int V4LDev::setImageSize(int w, int h) {

    if (w < _minWidth)
	w = _minWidth;

    if (h >= 0 && h < _minHeight)
	h = _minHeight;

    if (w > _maxWidth)
	w = _maxWidth;

    if (h > _maxHeight)
	h = _maxHeight;

    if (h == -1) {
	h = int(w/_aspectRatio);
    }

    while (w%4 != 0 && w > _minWidth)
	    w--;

    while (h%4 != 0 && h > _minHeight)
	    h--;

    _capH = h;
    _capW = w;

    struct video_window vw;

    int rc = ioctl(_fd, VIDIOCGWIN, &vw);
    if (rc < 0)
	return -1;

    vw.width = w;
    vw.height = h;
    vw.flags = 0;
    if (_type&VID_TYPE_CHROMAKEY) {
        kdDebug() << "Enabling chromakey for V4L overlay." << endl;
        vw.flags |= VIDEO_WINDOW_CHROMAKEY;
    }
    rc = ioctl(_fd, VIDIOCSWIN, &vw);
    if (rc < 0)
	return -1;

    rc = ioctl(_fd, VIDIOCGWIN, &vw);
    if (rc < 0)
	return -1;

    if (vw.width != (unsigned)w || vw.height != (unsigned)h)
	return -1;

    _grabNeedsInit = true;
    _grabW = w;
    _grabH = h;

    if (_overlaid) {
        stopCapture();
        startCapture(vw.x, vw.y);
    }

    delete[] _readBuf;
    _readBuf = new uchar[_grabW*_grabH*4];

    return 0;
}



int V4LDev::startCapture(int x, int y) {
    if (!canOverlay())
        return -1;

    if (_overlaid)
        return -1;

    int on = 1;

    struct video_window vw;

    memset(&vw, 0, sizeof(struct video_window));
    int rc = ioctl(_fd, VIDIOCGWIN, &vw);
    if (rc < 0) {
        perror("VIDIOCGWIN");
        return -1;
    }

    vw.x = x;
    vw.y = y;
    vw.flags = 0;
    vw.width = _grabW;
    vw.height = _grabH;
    //kdDebug() << "x=" << x << " y=" << y << " w=" << vw.width << " h=" << vw.height << endl;
    vw.flags = 0;
    if (_type&VID_TYPE_CHROMAKEY) {
        kdDebug() << "Enabling chromakey for V4L overlay." << endl;
        vw.flags |= VIDEO_WINDOW_CHROMAKEY;
    }
    vw.clipcount = 0;
    rc = ioctl(_fd, VIDIOCSWIN, &vw);
    if (rc < 0) {
        perror("VIDIOCSWIN");
        return -1;
    }

    rc = ioctl(_fd, VIDIOCCAPTURE, &on);
    if (rc < 0) {
        perror("VIDIOCCAPTURE");
        return -1;
    }

    _overlaid = true;
    return 0;
}



int V4LDev::stopCapture() {
    if (!_overlaid)
        return -1;

    int on = 0;

    int rc = ioctl(_fd, VIDIOCCAPTURE, &on);
    if (rc < 0) return -1;

    _overlaid = false;
    return 0;
}





#define CreateParmFunc(X,Y)				\
							\
int V4LDev::set##X(int x) {				\
struct video_picture vp;				\
							\
	int rc = ioctl(_fd, VIDIOCGPICT, &vp);		\
							\
	if (rc >= 0) {					\
		vp.##Y = x;				\
		rc = ioctl(_fd, VIDIOCSPICT, &vp);	\
		if (rc < 0)				\
			return -1;			\
	} else return -1;				\
							\
return 0;						\
}							\
							\
int V4LDev::##Y() const {				\
struct video_picture vp;				\
							\
	int rc = ioctl(_fd, VIDIOCGPICT, &vp);		\
							\
	if (rc >= 0) {					\
		return vp.##Y;				\
	}						\
							\
return -1;						\
}


CreateParmFunc(Brightness,brightness)
CreateParmFunc(Hue,hue)
CreateParmFunc(Colour,colour)
CreateParmFunc(Contrast,contrast)
CreateParmFunc(Whiteness,whiteness)


#undef CreateParmFunc


int V4LDev::enableAudio() {
    if (!_hasAudio) return -1;

    int rc;
    struct video_audio va;

    memset(&va, 0, sizeof(va));
    rc = ioctl(_fd, VIDIOCGAUDIO, &va);

    if (rc < 0) {
        perror("VIDIOCGAUDIO");
	return -1;
    }

    va.flags &= ~VIDEO_AUDIO_MUTE;
    rc = ioctl(_fd, VIDIOCSAUDIO, &va);
    if (rc < 0) {
        perror("VIDIOCSAUDIO");
	return -1;
    }

    return 0;
}


bool V4LDev::audioEnabled() const {
    if (!_hasAudio) return false;

    int rc;
    struct video_audio va;

    memset(&va, 0, sizeof(va));
    rc = ioctl(_fd, VIDIOCGAUDIO, &va);

    if (rc < 0) {
        perror("VIDIOCGAUDIO");
	return false;
    }

    return (va.flags & VIDEO_AUDIO_MUTE);
}


int V4LDev::disableAudio() {

    if (!_hasAudio) return -1;

    int rc;
    struct video_audio va;

    memset(&va, 0, sizeof(va));
    rc = ioctl(_fd, VIDIOCGAUDIO, &va);

    if (rc < 0) {
        perror("VIDIOCGAUDIO");
	return -1;
    }

    va.flags |= VIDEO_AUDIO_MUTE;
    rc = ioctl(_fd, VIDIOCSAUDIO, &va);
    if (rc < 0) {
        perror("VIDIOCSAUDIO");
	return -1;
    }

    return 0;
}


bool V4LDev::overlayOn() const {
    return _overlaid;
}


bool V4LDev::isTuner() const {
    return false;
}


bool V4LDev::isCamera() const {
    return false;
}


void V4LDev::addClip(const QRect& clip) {
    if (_clips.count() < MAX_CLIP_RECTS)
        _clips.append(clip);
}


void V4LDev::clearClips() {
    _clips.clear();
}


void V4LDev::reClip() {
    struct video_window vw;
    memset(&vw, 0, sizeof(vw));
    int rc = ioctl(_fd, VIDIOCGWIN, &vw);

    if (rc == 0) {
        for (unsigned int i = 0; i < _clips.count(); i++) {
            _cliprecs[i].x = _clips[i].x() - vw.x;
            _cliprecs[i].y = _clips[i].y() - vw.y;
            _cliprecs[i].width = _clips[i].width();
            _cliprecs[i].height = _clips[i].height();
        }

        vw.clips = _cliprecs;
        vw.clipcount = _clips.count();
        vw.flags = 0;
        if (_type&VID_TYPE_CHROMAKEY) {
            kdDebug() << "Enabling chromakey for V4L overlay." << endl;
            vw.flags |= VIDEO_WINDOW_CHROMAKEY;
        }
        ioctl(_fd, VIDIOCSWIN, &vw);
        if (_overlaid) {
            int one = 1;
            ioctl(_fd, VIDIOCCAPTURE, &one);
        }
    }
}


int V4LDev::setCaptureGeometry(const QRect& geom) {
    if (!canOverlay())
        return -1;

    struct video_window vw;

    memset(&vw, 0, sizeof(struct video_window));
    int rc = ioctl(_fd, VIDIOCGWIN, &vw);
    if (rc < 0) {
        perror("VIDIOCGWIN");
        return -1;
    }

    vw.x = geom.x();
    vw.y = geom.y();
    vw.flags = 0;
    if (_type&VID_TYPE_CHROMAKEY) {
        vw.flags |= VIDEO_WINDOW_CHROMAKEY;
    }
    rc = ioctl(_fd, VIDIOCSWIN, &vw);
    if (rc < 0) {
        perror("VIDIOCSWIN");
        return -1;
    }

return setImageSize(geom.width(), geom.height());
}

