/*------------------------------------------------------------------------
 *  Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
 *
 *  This file is part of the ZBar Bar Code Reader.
 *
 *  The ZBar Bar Code Reader is free software; you can redistribute it
 *  and/or modify it under the terms of the GNU Lesser Public License as
 *  published by the Free Software Foundation; either version 2.1 of
 *  the License, or (at your option) any later version.
 *
 *  The ZBar Bar Code Reader 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 Lesser Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser Public License
 *  along with the ZBar Bar Code Reader; if not, write to the Free
 *  Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 *  Boston, MA  02110-1301  USA
 *
 *  http://sourceforge.net/projects/zbar
 *------------------------------------------------------------------------*/

#include "video.h"
#include "image.h"

#ifdef HAVE_LIBJPEG
extern struct jpeg_decompress_struct *_zbar_jpeg_decomp_create(void);
extern void _zbar_jpeg_decomp_destroy(struct jpeg_decompress_struct *cinfo);
#endif

static void _zbar_video_recycle_image(zbar_image_t *img)
{
    zbar_video_t *vdo = img->src;
    assert(vdo);
    assert(img->srcidx >= 0);
    video_lock(vdo);
    if (vdo->images[img->srcidx] != img)
	vdo->images[img->srcidx] = img;
    if (vdo->active)
	vdo->nq(vdo, img);
    else
	video_unlock(vdo);
}

static void _zbar_video_recycle_shadow(zbar_image_t *img)
{
    zbar_video_t *vdo = img->src;
    assert(vdo);
    assert(img->srcidx == -1);
    video_lock(vdo);
    img->next	      = vdo->shadow_image;
    vdo->shadow_image = img;
    video_unlock(vdo);
}

zbar_video_t *zbar_video_create()
{
    zbar_video_t *vdo = calloc(1, sizeof(zbar_video_t));
    int i;
    if (!vdo)
	return (NULL);
    err_init(&vdo->err, ZBAR_MOD_VIDEO);
    vdo->fd = -1;

    (void)_zbar_mutex_init(&vdo->qlock);

    /* pre-allocate images */
    vdo->num_images = ZBAR_VIDEO_IMAGES_MAX;
    vdo->images	    = calloc(ZBAR_VIDEO_IMAGES_MAX, sizeof(zbar_image_t *));
    if (!vdo->images) {
	zbar_video_destroy(vdo);
	return (NULL);
    }

    for (i = 0; i < ZBAR_VIDEO_IMAGES_MAX; i++) {
	zbar_image_t *img = vdo->images[i] = zbar_image_create();
	if (!img) {
	    zbar_video_destroy(vdo);
	    return (NULL);
	}
	img->refcnt  = 0;
	img->cleanup = _zbar_video_recycle_image;
	img->srcidx  = i;
	img->src     = vdo;
    }

    return (vdo);
}

void zbar_video_destroy(zbar_video_t *vdo)
{
    if (vdo->intf != VIDEO_INVALID)
	zbar_video_open(vdo, NULL);
    if (vdo->images) {
	int i;
	for (i = 0; i < ZBAR_VIDEO_IMAGES_MAX; i++)
	    if (vdo->images[i])
		_zbar_image_free(vdo->images[i]);
	free(vdo->images);
    }
    while (vdo->shadow_image) {
	zbar_image_t *img = vdo->shadow_image;
	vdo->shadow_image = img->next;
	free((void *)img->data);
	img->data = NULL;
	free(img);
    }
    if (vdo->buf)
	free(vdo->buf);
    if (vdo->formats)
	free(vdo->formats);
    if (vdo->emu_formats)
	free(vdo->emu_formats);

    if (vdo->free)
	vdo->free(vdo);

    err_cleanup(&vdo->err);
    _zbar_mutex_destroy(&vdo->qlock);

#ifdef HAVE_LIBJPEG
    if (vdo->jpeg_img) {
	zbar_image_destroy(vdo->jpeg_img);
	vdo->jpeg_img = NULL;
    }
    if (vdo->jpeg) {
	_zbar_jpeg_decomp_destroy(vdo->jpeg);
	vdo->jpeg = NULL;
    }
#endif
    free(vdo);
}

int zbar_video_open(zbar_video_t *vdo, const char *dev)
{
    char *ldev = NULL;
    int rc;
    zbar_video_enable(vdo, 0);
    video_lock(vdo);
    if (vdo->intf != VIDEO_INVALID) {
	if (vdo->cleanup) {
	    vdo->cleanup(vdo);
	    vdo->cleanup = NULL;
	}
	zprintf(1, "closed camera (fd=%d)\n", vdo->fd);
	vdo->intf = VIDEO_INVALID;
    }
    video_unlock(vdo);

    if (!dev)
	return (0);

    if ((unsigned char)dev[0] < 0x10) {
	/* default linux device, overloaded for other platforms */
	int id = dev[0];
	dev = ldev = strdup("/dev/video0");
	ldev[10]   = '0' + id;
    }

    rc = _zbar_video_open(vdo, dev);

    if (ldev)
	free(ldev);
    return (rc);
}

int zbar_video_get_fd(const zbar_video_t *vdo)
{
    if (vdo->intf == VIDEO_INVALID)
	return (err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
			    "video device not opened"));
    if (vdo->intf != VIDEO_V4L2)
	return (err_capture(vdo, SEV_WARNING, ZBAR_ERR_UNSUPPORTED, __func__,
			    "video driver does not support polling"));
    return (vdo->fd);
}

int zbar_video_request_size(zbar_video_t *vdo, unsigned width, unsigned height)
{
    if (vdo->initialized)
	/* FIXME re-init different format? */
	return (err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
			    "already initialized, unable to resize"));

    vdo->width	= width;
    vdo->height = height;
    zprintf(1, "request size: %d x %d\n", width, height);
    return (0);
}

int zbar_video_request_interface(zbar_video_t *vdo, int ver)
{
    if (vdo->intf != VIDEO_INVALID)
	return (
	    err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
			"device already opened, unable to change interface"));
    vdo->intf = (video_interface_t)ver;
    zprintf(1, "request interface version %d\n", vdo->intf);
    return (0);
}

int zbar_video_request_iomode(zbar_video_t *vdo, int iomode)
{
    if (vdo->intf != VIDEO_INVALID)
	return (err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
			    "device already opened, unable to change iomode"));
    if (iomode < 0 || iomode > VIDEO_USERPTR)
	return (err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
			    "invalid iomode requested"));
    vdo->iomode = iomode;
    return (0);
}

int zbar_video_get_width(const zbar_video_t *vdo)
{
    return (vdo->width);
}

int zbar_video_get_height(const zbar_video_t *vdo)
{
    return (vdo->height);
}

uint32_t zbar_video_get_format(const zbar_video_t *vdo)
{
    return (vdo->format);
}

static inline int video_init_images(zbar_video_t *vdo)
{
    int i;
    assert(vdo->datalen);
    if (vdo->iomode != VIDEO_MMAP) {
	assert(!vdo->buf);
	vdo->buflen = vdo->num_images * vdo->datalen;
	vdo->buf    = calloc(1, vdo->buflen);
	if (!vdo->buf)
	    return (err_capture(vdo, SEV_FATAL, ZBAR_ERR_NOMEM, __func__,
				"unable to allocate image buffers"));
	zprintf(1, "pre-allocated %d %s buffers size=0x%lx\n", vdo->num_images,
		(vdo->iomode == VIDEO_READWRITE) ? "READ" : "USERPTR",
		vdo->buflen);
    }
    for (i = 0; i < vdo->num_images; i++) {
	zbar_image_t *img = vdo->images[i];
	img->format	  = vdo->format;
	zbar_image_set_size(img, vdo->width, vdo->height);
	if (vdo->iomode != VIDEO_MMAP) {
	    unsigned long offset = i * vdo->datalen;
	    img->datalen	 = vdo->datalen;
	    img->data		 = (uint8_t *)vdo->buf + offset;
	    zprintf(2, "    [%02d] @%08lx\n", i, offset);
	}
    }
    return (0);
}

int zbar_video_init(zbar_video_t *vdo, unsigned long fmt)
{
#ifdef HAVE_LIBJPEG
    const zbar_format_def_t *vidfmt;
#endif
    if (vdo->initialized)
	/* FIXME re-init different format? */
	return (err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
			    "already initialized, re-init unimplemented"));

    if (vdo->init(vdo, fmt))
	return (-1);
    vdo->format = fmt;
    if (video_init_images(vdo))
	return (-1);
#ifdef HAVE_LIBJPEG
    vidfmt = _zbar_format_lookup(fmt);
    if (vidfmt && vidfmt->group == ZBAR_FMT_JPEG) {
	zbar_image_t *img;
	/* prepare for decoding */
	if (!vdo->jpeg)
	    vdo->jpeg = _zbar_jpeg_decomp_create();
	if (vdo->jpeg_img)
	    zbar_image_destroy(vdo->jpeg_img);

	/* create intermediate image for decoder to use*/
	img = vdo->jpeg_img = zbar_image_create();
	img->format	    = fourcc('Y', '8', '0', '0');
	zbar_image_set_size(img, vdo->width, vdo->height);
	img->datalen = vdo->width * vdo->height;
    }
#endif
    vdo->initialized = 1;
    return (0);
}

int zbar_video_enable(zbar_video_t *vdo, int enable)
{
    if (vdo->active == enable)
	return (0);

    if (enable) {
	if (vdo->intf == VIDEO_INVALID)
	    return (err_capture(vdo, SEV_ERROR, ZBAR_ERR_INVALID, __func__,
				"video device not opened"));

	if (!vdo->initialized && zbar_negotiate_format(vdo, NULL))
	    return (-1);
    }

    if (video_lock(vdo))
	return (-1);
    vdo->active = enable;
    if (enable) {
	/* enqueue all buffers */
	int i;
	for (i = 0; i < vdo->num_images; i++)
	    if (vdo->nq(vdo, vdo->images[i]) ||
		((i + 1 < vdo->num_images) && video_lock(vdo)))
		return (-1);

	return (vdo->start(vdo));
    } else {
	int i;
	for (i = 0; i < vdo->num_images; i++)
	    vdo->images[i]->next = NULL;
	vdo->nq_image = vdo->dq_image = NULL;
	if (video_unlock(vdo))
	    return (-1);

	return (vdo->stop(vdo));
    }
}

zbar_image_t *zbar_video_next_image(zbar_video_t *vdo)
{
    unsigned frame;
    zbar_image_t *img;

    if (video_lock(vdo))
	return (NULL);
    if (!vdo->active) {
	video_unlock(vdo);
	return (NULL);
    }

    frame = vdo->frame++;
    img	  = vdo->dq(vdo);
    if (img) {
	img->seq = frame;
	if (vdo->num_images < 2) {
	    /* return a *copy* of the video image and immediately recycle
             * the driver's buffer to avoid deadlocking the resources
             */
	    zbar_image_t *tmp = img;
	    video_lock(vdo);
	    img		      = vdo->shadow_image;
	    vdo->shadow_image = (img) ? img->next : NULL;
	    video_unlock(vdo);

	    if (!img) {
		img = zbar_image_create();
		assert(img);
		img->refcnt = 0;
		img->src    = vdo;
		/* recycle the shadow images */

		img->format = vdo->format;
		zbar_image_set_size(img, vdo->width, vdo->height);
		img->datalen = vdo->datalen;
		img->data    = malloc(vdo->datalen);
	    }
	    img->cleanup = _zbar_video_recycle_shadow;
	    img->seq	 = frame;
	    memcpy((void *)img->data, tmp->data, img->datalen);
	    _zbar_video_recycle_image(tmp);
	} else
	    img->cleanup = _zbar_video_recycle_image;
	_zbar_image_refcnt(img, 1);
    }
    return (img);
}

/** @brief return if fun unsupported, otherwise continue */
#define return_if_not_supported(fun, name)                            \
    {                                                                 \
	if (!(fun)) {                                                 \
	    zprintf(1, "video driver does not implement %s\n", name); \
	    return ZBAR_ERR_UNSUPPORTED;                              \
	}                                                             \
    }
#define return_if_non_zero(a) \
    {                         \
	int rv = a;           \
	if (rv != 0)          \
	    return (rv);      \
    }

int zbar_video_set_control(zbar_video_t *vdo, const char *control_name,
			   int value)
{
    int loc_value, rv;
    return_if_not_supported(vdo->set_control, "set_control");
    loc_value = value;

    rv = vdo->set_control(vdo, control_name, &loc_value);

    if (rv == 0)
	zprintf(1, "value of %s set to: %d\n", control_name, loc_value);
    return (rv);
}

int zbar_video_get_control(zbar_video_t *vdo, const char *control_name,
			   int *value)
{
    return_if_not_supported(vdo->get_control, "get_control");
    return (vdo->get_control(vdo, control_name, value));
}

struct video_controls_s *zbar_video_get_controls(const zbar_video_t *vdo,
						 int index)
{
    int i		       = 0;
    struct video_controls_s *p = vdo->controls;

    while (p && i != index) {
	i++;
	p = p->next;
    }

    if (!p)
	return NULL;

    return p;
}

struct video_resolution_s *zbar_video_get_resolutions(const zbar_video_t *vdo,
						      int index)
{
    int i			 = 0;
    struct video_resolution_s *p = vdo->res;

    while (i != index) {
	if (!p->width || !p->height)
	    return NULL;
	i++;
	p++;
    }

    if (!p->width || !p->height)
	return NULL;

    return p;
}
