/*
 *  Copyright 1994-2022 Olivier Girondel
 *
 *  This file is part of lebiniou.
 *
 *  lebiniou is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  lebiniou 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with lebiniou. If not, see <http://www.gnu.org/licenses/>.
 */

#include "webcam.h"


extern char *video_base;


int
xioctl(int fd, int request, void *arg)
{
  int r;

  do {
    r = ioctl(fd, request, arg);
  } while ((-1 == r) && (EINTR == errno));

  return r;
}


void
init_read(unsigned int buffer_size, webcam_t *cam)
{
  cam->buffers = xcalloc(1, sizeof(buffer_t));
  cam->buffers[0].length = buffer_size;
  cam->buffers[0].start = xmalloc(buffer_size);
}


void
init_mmap(webcam_t *cam)
{
  struct v4l2_requestbuffers req;

  CLEAR(req);

  req.count               = WEBCAM_MMAP;
  req.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  req.memory              = V4L2_MEMORY_MMAP;

  if (-1 == xioctl(cam->fd, VIDIOC_REQBUFS, &req)) {
    if (EINVAL == errno) {
      xerror("%s%d does not support memory mapping\n", video_base, cam->cam_no);
    } else {
      xperror("VIDIOC_REQBUFS");
    }
  }

  if (req.count < 2) {
    xerror("Insufficient buffer memory on %s%d\n", video_base, cam->cam_no);
  }

  cam->buffers = xcalloc(req.count, sizeof(buffer_t));

  for (cam->n_buffers = 0; cam->n_buffers < (int)req.count; ++cam->n_buffers) {
    struct v4l2_buffer buf;

    CLEAR(buf);

    buf.type        = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory      = V4L2_MEMORY_MMAP;
    buf.index       = cam->n_buffers;

    if (-1 == xioctl(cam->fd, VIDIOC_QUERYBUF, &buf)) {
      xperror("VIDIOC_QUERYBUF");
    }

    cam->buffers[cam->n_buffers].length = buf.length;
    cam->buffers[cam->n_buffers].start =
      mmap(NULL /* start anywhere */,
           buf.length,
           PROT_READ | PROT_WRITE /* required */,
           MAP_SHARED /* recommended */,
           cam->fd, buf.m.offset);

    if (MAP_FAILED == cam->buffers[cam->n_buffers].start) {
      xperror("mmap");
    }
  }
}


int
init_device(webcam_t *cam)
{
  struct v4l2_capability cap;
  struct v4l2_cropcap cropcap;
  struct v4l2_crop crop;
  struct v4l2_format fmt;

  if (-1 == xioctl(cam->fd, VIDIOC_QUERYCAP, &cap)) {
    if (EINVAL == errno) {
      fprintf(stderr, "[!] %s%d is no V4L2 device\n", video_base, cam->cam_no);
      return -1;
    } else {
      xperror("VIDIOC_QUERYCAP");
    }
  }

  if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
    fprintf(stderr, "[!] %s%d is no video capture device\n", video_base, cam->cam_no);
    return -1;
  }

  switch (cam->io) {
    case IO_METHOD_READ:
      if (!(cap.capabilities & V4L2_CAP_READWRITE)) {
        fprintf(stderr, "[!] %s%d does not support read i/o\n", video_base, cam->cam_no);
        return -1;
      }
      break;

    case IO_METHOD_MMAP:
      if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
        fprintf(stderr, "[!] %s%d does not support streaming i/o\n", video_base, cam->cam_no);
        return -1;
      }
      break;
  }

  /* Select video input, video standard and tune here. */

  CLEAR(cropcap);

  cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

  if (0 == xioctl(cam->fd, VIDIOC_CROPCAP, &cropcap)) {
    crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    crop.c.width = CAP_WIDTH;
    crop.c.height = CAP_HEIGHT;
    crop.c.left = 0;
    crop.c.top = 0;

    if (-1 == xioctl(cam->fd, VIDIOC_S_CROP, &crop)) {
      switch (errno) {
        case EINVAL:
          /* Cropping not supported. */
          break;

        default:
          /* Errors ignored. */
          break;
      }
    }
  } else {
    /* Errors ignored. */
  }

  CLEAR(fmt);

  fmt.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  fmt.fmt.pix.width       = CAP_WIDTH;
  fmt.fmt.pix.height      = CAP_HEIGHT;
  fmt.fmt.win.w.width     = CAP_WIDTH;
  fmt.fmt.win.w.height    = CAP_HEIGHT;
  fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
  fmt.fmt.pix.field       = V4L2_FIELD_INTERLACED;

  /* Note VIDIOC_S_FMT may change width and height. */
  if (-1 == xioctl(cam->fd, VIDIOC_S_FMT, &fmt)) {
    fprintf(stderr, "[!] Webcam #%d: could not set capture format\n", cam->cam_no);
    return -1;
  }

#if 0
  /* Buggy driver paranoia. */
  min = fmt.fmt.pix.width * 2;
  if (fmt.fmt.pix.bytesperline < min) {
    fmt.fmt.pix.bytesperline = min;
  }
  min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
  if (fmt.fmt.pix.sizeimage < min) {
    fmt.fmt.pix.sizeimage = min;
  }
#endif

  switch (cam->io) {
    case IO_METHOD_READ:
      init_read(fmt.fmt.pix.sizeimage, cam);
      break;

    case IO_METHOD_MMAP:
      init_mmap(cam);
      break;
  }
#ifdef DEBUG
  printf("[i] Webcam %d initialized\n", cam->cam_no);
#endif

  return 0;
}


void
uninit_device(webcam_t *cam)
{
  int i;

  switch (cam->io) {
    case IO_METHOD_READ:
      xfree(cam->buffers[0].start);
      break;

    case IO_METHOD_MMAP:
      for (i = 0; i < cam->n_buffers; ++i)
        if (-1 == munmap(cam->buffers[i].start, cam->buffers[i].length)) {
          xperror("munmap");
        }
      break;
  }

  xfree(cam->buffers);
}
