/*
 *  Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include "modules/video_capture/linux/video_capture_v4l2.h"

#include <fcntl.h>
#if defined(__NetBSD__) || defined(__OpenBSD__) // WEBRTC_BSD
#include <sys/videoio.h>
#elif defined(__sun)
#include <sys/videodev2.h>
#else
#include <linux/videodev2.h>
#endif
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <unistd.h>

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>

#include "api/sequence_checker.h"
#include "common_video/libyuv/include/webrtc_libyuv.h"
#include "media/base/video_common.h"
#include "modules/video_capture/video_capture_defines.h"
#include "modules/video_capture/video_capture_impl.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/platform_thread.h"
#include "rtc_base/race_checker.h"
#include "rtc_base/synchronization/mutex.h"
#include "system_wrappers/include/clock.h"

// These defines are here to support building on kernel 3.16 which some
// downstream projects, e.g. Firefox, use.
// TODO(apehrson): Remove them and their undefs when no longer needed.
#ifndef V4L2_PIX_FMT_ABGR32
#define ABGR32_OVERRIDE 1
#define V4L2_PIX_FMT_ABGR32 v4l2_fourcc('A', 'R', '2', '4')
#endif

#ifndef V4L2_PIX_FMT_ARGB32
#define ARGB32_OVERRIDE 1
#define V4L2_PIX_FMT_ARGB32 v4l2_fourcc('B', 'A', '2', '4')
#endif

#ifndef V4L2_PIX_FMT_RGBA32
#define RGBA32_OVERRIDE 1
#define V4L2_PIX_FMT_RGBA32 v4l2_fourcc('A', 'B', '2', '4')
#endif

namespace webrtc {
namespace videocapturemodule {
VideoCaptureModuleV4L2::VideoCaptureModuleV4L2(Clock* clock)
    : VideoCaptureImpl(clock),
      _deviceId(-1),
      _deviceFd(-1),
      _buffersAllocatedByDevice(-1),
      _streaming(false),
      _captureStarted(false),
      _pool(nullptr) {}

int32_t VideoCaptureModuleV4L2::Init(const char* deviceUniqueIdUTF8) {
  RTC_DCHECK_RUN_ON(&api_checker_);

  int len = strlen((const char*)deviceUniqueIdUTF8);
  _deviceUniqueId = new (std::nothrow) char[len + 1];
  if (_deviceUniqueId) {
    memcpy(_deviceUniqueId, deviceUniqueIdUTF8, len + 1);
  }

  int fd;
  char device[32];
  bool found = false;

  /* detect /dev/video [0-63] entries */
  int n;
  for (n = 0; n < 64; n++) {
    snprintf(device, sizeof(device), "/dev/video%d", n);
    if ((fd = open(device, O_RDONLY)) != -1) {
      // query device capabilities
      struct v4l2_capability cap;
      if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
        if (cap.bus_info[0] != 0) {
          if (strncmp((const char*)cap.bus_info,
                      (const char*)deviceUniqueIdUTF8,
                      strlen((const char*)deviceUniqueIdUTF8)) ==
              0) {  // match with device id
            close(fd);
            found = true;
            break;  // fd matches with device unique id supplied
          }
        }
      }
      close(fd);  // close since this is not the matching device
    }
  }
  if (!found) {
    RTC_LOG(LS_INFO) << "no matching device found";
    return -1;
  }
  _deviceId = n;  // store the device id
  return 0;
}

VideoCaptureModuleV4L2::~VideoCaptureModuleV4L2() {
  RTC_DCHECK_RUN_ON(&api_checker_);

  StopCapture();

  RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);
  if (_deviceFd != -1)
    close(_deviceFd);
}

int32_t VideoCaptureModuleV4L2::StartCapture(
    const VideoCaptureCapability& capability) {
  RTC_DCHECK_RUN_ON(&api_checker_);

  if (_captureStarted) {
    if (capability == _requestedCapability) {
      return 0;
    } else {
      StopCapture();
    }
  }

  // We don't want members above to be guarded by capture_checker_ as
  // it's meant to be for members that are accessed on the API thread
  // only when we are not capturing. The code above can be called many
  // times while sharing instance of VideoCaptureV4L2 between websites
  // and therefore it would not follow the requirements of this checker.
  {
    RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);

    // Set a baseline of configured parameters. It is updated here during
    // configuration, then read from the capture thread.
    configured_capability_ = capability;

    MutexLock lock(&capture_lock_);
    // first open /dev/video device
    char device[20];
    snprintf(device, sizeof(device), "/dev/video%d", _deviceId);

    if ((_deviceFd = open(device, O_RDWR | O_NONBLOCK, 0)) < 0) {
      RTC_LOG(LS_INFO) << "error in opening " << device
                       << " errono = " << errno;
      return -1;
    }

    // Supported video formats in preferred order.
    // If the requested resolution is larger than VGA, we prefer MJPEG. Go for
    // I420 otherwise.
    unsigned int hdFmts[] = {
        V4L2_PIX_FMT_MJPEG,  V4L2_PIX_FMT_YUV420, V4L2_PIX_FMT_YVU420,
        V4L2_PIX_FMT_YUYV,   V4L2_PIX_FMT_UYVY,   V4L2_PIX_FMT_NV12,
        V4L2_PIX_FMT_ABGR32, V4L2_PIX_FMT_ARGB32, V4L2_PIX_FMT_RGBA32,
        V4L2_PIX_FMT_BGR32,  V4L2_PIX_FMT_RGB32,  V4L2_PIX_FMT_BGR24,
        V4L2_PIX_FMT_RGB24,  V4L2_PIX_FMT_RGB565, V4L2_PIX_FMT_JPEG,
    };
    unsigned int sdFmts[] = {
        V4L2_PIX_FMT_YUV420, V4L2_PIX_FMT_YVU420, V4L2_PIX_FMT_YUYV,
        V4L2_PIX_FMT_UYVY,   V4L2_PIX_FMT_NV12,   V4L2_PIX_FMT_ABGR32,
        V4L2_PIX_FMT_ARGB32, V4L2_PIX_FMT_RGBA32, V4L2_PIX_FMT_BGR32,
        V4L2_PIX_FMT_RGB32,  V4L2_PIX_FMT_BGR24,  V4L2_PIX_FMT_RGB24,
        V4L2_PIX_FMT_RGB565, V4L2_PIX_FMT_MJPEG,  V4L2_PIX_FMT_JPEG,
    };
    const bool isHd = capability.width > 640 || capability.height > 480;
    unsigned int* fmts = isHd ? hdFmts : sdFmts;
    static_assert(sizeof(hdFmts) == sizeof(sdFmts));
    constexpr int nFormats = sizeof(hdFmts) / sizeof(unsigned int);

    // Enumerate image formats.
    struct v4l2_fmtdesc fmt;
    int fmtsIdx = nFormats;
    memset(&fmt, 0, sizeof(fmt));
    fmt.index = 0;
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    RTC_LOG(LS_INFO) << "Video Capture enumerats supported image formats:";
    while (ioctl(_deviceFd, VIDIOC_ENUM_FMT, &fmt) == 0) {
      RTC_LOG(LS_INFO) << "  { pixelformat = " << GetFourccName(fmt.pixelformat)
                       << ", description = '" << fmt.description << "' }";
      // Match the preferred order.
      for (int i = 0; i < nFormats; i++) {
        if (fmt.pixelformat == fmts[i] && i < fmtsIdx)
          fmtsIdx = i;
      }
      // Keep enumerating.
      fmt.index++;
    }

    if (fmtsIdx == nFormats) {
      RTC_LOG(LS_INFO) << "no supporting video formats found";
      return -1;
    } else {
      RTC_LOG(LS_INFO) << "We prefer format " << GetFourccName(fmts[fmtsIdx]);
    }

    struct v4l2_format video_fmt;
    memset(&video_fmt, 0, sizeof(struct v4l2_format));
    video_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    video_fmt.fmt.pix.sizeimage = 0;
    video_fmt.fmt.pix.width = capability.width;
    video_fmt.fmt.pix.height = capability.height;
    video_fmt.fmt.pix.pixelformat = fmts[fmtsIdx];

    if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV)
      configured_capability_.videoType = VideoType::kYUY2;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUV420)
      configured_capability_.videoType = VideoType::kI420;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YVU420)
      configured_capability_.videoType = VideoType::kYV12;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_UYVY)
      configured_capability_.videoType = VideoType::kUYVY;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_NV12)
      configured_capability_.videoType = VideoType::kNV12;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_BGR24)
      configured_capability_.videoType = VideoType::kRGB24;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_RGB24)
      configured_capability_.videoType = VideoType::kBGR24;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_RGB565)
      configured_capability_.videoType = VideoType::kRGB565;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_ABGR32 ||
             video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_BGR32)
      configured_capability_.videoType = VideoType::kARGB;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_ARGB32 ||
             video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_RGB32)
      configured_capability_.videoType = VideoType::kBGRA;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_RGBA32)
      configured_capability_.videoType = VideoType::kABGR;
    else if (video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG ||
             video_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_JPEG)
      configured_capability_.videoType = VideoType::kMJPEG;
    else
      RTC_DCHECK_NOTREACHED();

    // set format and frame size now
    if (ioctl(_deviceFd, VIDIOC_S_FMT, &video_fmt) < 0) {
      RTC_LOG(LS_INFO) << "error in VIDIOC_S_FMT, errno = " << errno;
      return -1;
    }

    // initialize current width and height
    configured_capability_.width = video_fmt.fmt.pix.width;
    configured_capability_.height = video_fmt.fmt.pix.height;

    // Trying to set frame rate, before check driver capability.
    bool driver_framerate_support = true;
    struct v4l2_streamparm streamparms;
    memset(&streamparms, 0, sizeof(streamparms));
    streamparms.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(_deviceFd, VIDIOC_G_PARM, &streamparms) < 0) {
      RTC_LOG(LS_INFO) << "error in VIDIOC_G_PARM errno = " << errno;
      driver_framerate_support = false;
      // continue
    } else {
      // check the capability flag is set to V4L2_CAP_TIMEPERFRAME.
      if (streamparms.parm.capture.capability & V4L2_CAP_TIMEPERFRAME) {
        // driver supports the feature. Set required framerate.
        memset(&streamparms, 0, sizeof(streamparms));
        streamparms.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        streamparms.parm.capture.timeperframe.numerator = 1;
        streamparms.parm.capture.timeperframe.denominator = capability.maxFPS;
        if (ioctl(_deviceFd, VIDIOC_S_PARM, &streamparms) < 0) {
          RTC_LOG(LS_INFO) << "Failed to set the framerate. errno=" << errno;
          driver_framerate_support = false;
        }
      }
    }
    // If driver doesn't support framerate control, need to hardcode.
    // Hardcoding the value based on the frame size.
    if (!driver_framerate_support) {
      if (configured_capability_.width >= 800 &&
          configured_capability_.videoType != VideoType::kMJPEG) {
        configured_capability_.maxFPS = 15;
      } else {
        configured_capability_.maxFPS = 30;
      }
    }

    if (!AllocateVideoBuffers()) {
      RTC_LOG(LS_INFO) << "failed to allocate video capture buffers";
      return -1;
    }

    // Needed to start UVC camera - from the uvcview application
    enum v4l2_buf_type type;
    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(_deviceFd, VIDIOC_STREAMON, &type) == -1) {
      RTC_LOG(LS_INFO) << "Failed to turn on stream";
      return -1;
    }

    _requestedCapability = capability;
    _captureStarted = true;
    _streaming = true;
    quit_ = false;
  }
  // Outside of `RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);`

  // start capture thread;
  RTC_DCHECK(_captureThread.empty());
  _captureThread = PlatformThread::SpawnJoinable(
      [self = scoped_refptr(this)] {
        while (self->CaptureProcess()) {
        }
      },
      "CaptureThread", ThreadAttributes().SetPriority(ThreadPriority::kHigh));

  return 0;
}

int32_t VideoCaptureModuleV4L2::StopCapture() {
  RTC_DCHECK_RUN_ON(&api_checker_);

  if (!_captureThread.empty()) {
    {
      MutexLock lock(&capture_lock_);
      quit_ = true;
    }
    // Make sure the capture thread stops using the mutex.
    _captureThread.Finalize();
  }

  _captureStarted = false;

  RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);
  MutexLock lock(&capture_lock_);
  if (_streaming) {
    _streaming = false;

    DeAllocateVideoBuffers();
    close(_deviceFd);
    _deviceFd = -1;

    _requestedCapability = configured_capability_ = VideoCaptureCapability();
  }

  return 0;
}

// critical section protected by the caller

bool VideoCaptureModuleV4L2::AllocateVideoBuffers() {
  RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);
  struct v4l2_requestbuffers rbuffer;
  memset(&rbuffer, 0, sizeof(v4l2_requestbuffers));

  rbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  rbuffer.memory = V4L2_MEMORY_MMAP;
  rbuffer.count = kNoOfV4L2Bufffers;

  if (ioctl(_deviceFd, VIDIOC_REQBUFS, &rbuffer) < 0) {
    RTC_LOG(LS_INFO) << "Could not get buffers from device. errno = " << errno;
    return false;
  }

  if (rbuffer.count > kNoOfV4L2Bufffers)
    rbuffer.count = kNoOfV4L2Bufffers;

  _buffersAllocatedByDevice = rbuffer.count;

  // Map the buffers
  _pool = new Buffer[rbuffer.count];

  for (unsigned int i = 0; i < rbuffer.count; i++) {
    struct v4l2_buffer buffer;
    memset(&buffer, 0, sizeof(v4l2_buffer));
    buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buffer.memory = V4L2_MEMORY_MMAP;
    buffer.index = i;

    if (ioctl(_deviceFd, VIDIOC_QUERYBUF, &buffer) < 0) {
      return false;
    }

    _pool[i].start = mmap(nullptr, buffer.length, PROT_READ | PROT_WRITE,
                          MAP_SHARED, _deviceFd, buffer.m.offset);

    if (MAP_FAILED == _pool[i].start) {
      for (unsigned int j = 0; j < i; j++)
        munmap(_pool[j].start, _pool[j].length);
      return false;
    }

    _pool[i].length = buffer.length;

    if (ioctl(_deviceFd, VIDIOC_QBUF, &buffer) < 0) {
      return false;
    }
  }
  return true;
}

bool VideoCaptureModuleV4L2::DeAllocateVideoBuffers() {
  RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);
  // unmap buffers
  for (int i = 0; i < _buffersAllocatedByDevice; i++)
    munmap(_pool[i].start, _pool[i].length);

  delete[] _pool;

  // turn off stream
  enum v4l2_buf_type type;
  type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  if (ioctl(_deviceFd, VIDIOC_STREAMOFF, &type) < 0) {
    RTC_LOG(LS_INFO) << "VIDIOC_STREAMOFF error. errno: " << errno;
  }

  return true;
}

bool VideoCaptureModuleV4L2::CaptureStarted() {
  RTC_DCHECK_RUN_ON(&api_checker_);
  return _captureStarted;
}

bool VideoCaptureModuleV4L2::CaptureProcess() {
  RTC_CHECK_RUNS_SERIALIZED(&capture_checker_);

  int retVal = 0;
  struct pollfd rSet;

  rSet.fd = _deviceFd;
  rSet.events = POLLIN;
  rSet.revents = 0;

  retVal = poll(&rSet, 1, 1000);

  {
    MutexLock lock(&capture_lock_);

    if (quit_) {
      return false;
    }

    if (retVal < 0 && errno != EINTR) {  // continue if interrupted
      // poll failed
      return false;
    } else if (retVal == 0) {
      // poll timed out
      return true;
    } else if (!(rSet.revents & POLLIN)) {
      // not event on camera handle
      return true;
    }

    if (_streaming) {
      struct v4l2_buffer buf;
      memset(&buf, 0, sizeof(struct v4l2_buffer));
      buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
      buf.memory = V4L2_MEMORY_MMAP;
      // dequeue a buffer - repeat until dequeued properly!
      while (ioctl(_deviceFd, VIDIOC_DQBUF, &buf) < 0) {
        if (errno != EINTR) {
          RTC_LOG(LS_INFO) << "could not sync on a buffer on device "
                           << strerror(errno);
          return true;
        }
      }

      // convert to to I420 if needed
      IncomingFrame(reinterpret_cast<uint8_t*>(_pool[buf.index].start),
                    buf.bytesused, configured_capability_);
      // enqueue the buffer again
      if (ioctl(_deviceFd, VIDIOC_QBUF, &buf) == -1) {
        RTC_LOG(LS_INFO) << "Failed to enqueue capture buffer";
      }
    }
  }
  usleep(0);
  return true;
}

int32_t VideoCaptureModuleV4L2::CaptureSettings(
    VideoCaptureCapability& settings) {
  RTC_DCHECK_RUN_ON(&api_checker_);
  settings = _requestedCapability;

  return 0;
}
}  // namespace videocapturemodule
}  // namespace webrtc

#ifdef ABGR32_OVERRIDE
#undef ABGR32_OVERRIDE
#undef V4L2_PIX_FMT_ABGR32
#endif

#ifdef ARGB32_OVERRIDE
#undef ARGB32_OVERRIDE
#undef V4L2_PIX_FMT_ARGB32
#endif

#ifdef RGBA32_OVERRIDE
#undef RGBA32_OVERRIDE
#undef V4L2_PIX_FMT_RGBA32
#endif
