// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/chromeos/login/camera.h"

#include <stdlib.h>
#include <fcntl.h>  // low-level i/o
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>  // for videodev2.h
#include <linux/videodev2.h>

#include <algorithm>
#include <vector>

#include "base/logging.h"
#include "base/string_util.h"
#include "base/stringprintf.h"
#include "base/threading/thread.h"
#include "base/time.h"
#include "content/browser/browser_thread.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColorPriv.h"
#include "ui/gfx/size.h"

namespace chromeos {

namespace {

// Logs errno number and its text.
void log_errno(const std::string& message) {
  LOG(ERROR) << message << " errno: " << errno << ", " << strerror(errno);
}

// Helpful wrapper around ioctl that retries it upon failure in cases when
// this is appropriate.
int xioctl(int fd, int request, void* arg) {
  int r;
  do {
    r = ioctl(fd, request, arg);
  } while (r == -1 && errno == EINTR);
  return r;
}

// Clips integer value to 1 byte boundaries. Saturates the result on
// overflow or underflow.
uint8_t clip_to_byte(int value) {
  if (value > 255)
    value = 255;
  if (value < 0)
    value = 0;
  return static_cast<uint8_t>(value);
}

// Converts color from YUV colorspace to RGB. Returns the result in Skia
// format suitable for use with SkBitmap. For the formula see
// "Converting between YUV and RGB" article on MSDN:
// http://msdn.microsoft.com/en-us/library/ms893078.aspx
uint32_t convert_yuv_to_rgba(int y, int u, int v) {
  int c = y - 16;
  int d = u - 128;
  int e = v - 128;
  uint8_t r = clip_to_byte((298 * c + 409 * e + 128) >> 8);
  uint8_t g = clip_to_byte((298 * c - 100 * d - 208 * e + 128) >> 8);
  uint8_t b = clip_to_byte((298 * c + 516 * d + 128) >> 8);
  return SkPackARGB32(255U, r, g, b);
}

// Enumerates available frame sizes for specified pixel format and picks up the
// best one to set for the desired image resolution.
gfx::Size get_best_frame_size(int fd,
                              int pixel_format,
                              int desired_width,
                              int desired_height) {
  v4l2_frmsizeenum size = {};
  size.index = 0;
  size.pixel_format = pixel_format;
  std::vector<gfx::Size> sizes;
  int r = xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &size);
  while (r != -1) {
    if (size.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
      sizes.push_back(gfx::Size(size.discrete.width, size.discrete.height));
    }
    ++size.index;
    r = xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &size);
  }
  if (sizes.empty()) {
    NOTREACHED();
    return gfx::Size(desired_width, desired_height);
  }
  for (size_t i = 0; i < sizes.size(); ++i) {
    if (sizes[i].width() >= desired_width &&
        sizes[i].height() >= desired_height)
      return sizes[i];
  }
  // If higher resolution is not available, choose the highest available.
  size_t max_size_index = 0;
  int max_area = sizes[0].GetArea();
  for (size_t i = 1; i < sizes.size(); ++i) {
    if (sizes[i].GetArea() > max_area) {
      max_size_index = i;
      max_area = sizes[i].GetArea();
    }
  }
  return sizes[max_size_index];
}

// Default camera device name.
const char kDeviceName[] = "/dev/video0";
// Default width of each frame received from the camera.
const int kFrameWidth = 640;
// Default height of each frame received from the camera.
const int kFrameHeight = 480;
// Number of buffers to request from the device.
const int kRequestBuffersCount = 4;
// Timeout for select() call in microseconds.
const long kSelectTimeout = 1 * base::Time::kMicrosecondsPerSecond;

}  // namespace

///////////////////////////////////////////////////////////////////////////////
// Camera, public members:

Camera::Camera(Delegate* delegate, base::Thread* thread, bool mirrored)
    : delegate_(delegate),
      thread_(thread),
      device_name_(kDeviceName),
      device_descriptor_(-1),
      is_capturing_(false),
      desired_width_(kFrameWidth),
      desired_height_(kFrameHeight),
      frame_width_(kFrameWidth),
      frame_height_(kFrameHeight),
      mirrored_(mirrored) {
}

Camera::~Camera() {
  DCHECK_EQ(-1, device_descriptor_) << "Don't forget to uninitialize camera.";
}

void Camera::ReportFailure() {
  DCHECK(IsOnCameraThread());
  if (device_descriptor_ == -1) {
    BrowserThread::PostTask(
        BrowserThread::UI,
        FROM_HERE,
        NewRunnableMethod(this,
                          &Camera::OnInitializeFailure));
  } else if (!is_capturing_) {
    BrowserThread::PostTask(
        BrowserThread::UI,
        FROM_HERE,
        NewRunnableMethod(this,
                          &Camera::OnStartCapturingFailure));
  } else {
    BrowserThread::PostTask(
        BrowserThread::UI,
        FROM_HERE,
        NewRunnableMethod(this,
                          &Camera::OnCaptureFailure));
  }
}

void Camera::Initialize(int desired_width, int desired_height) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  PostCameraTask(
      FROM_HERE,
      NewRunnableMethod(this,
                        &Camera::DoInitialize,
                        desired_width,
                        desired_height));
}

void Camera::DoInitialize(int desired_width, int desired_height) {
  DCHECK(IsOnCameraThread());

  if (device_descriptor_ != -1) {
    LOG(WARNING) << "Camera is initialized already.";
    return;
  }

  int fd = OpenDevice(device_name_.c_str());
  if (fd == -1) {
    ReportFailure();
    return;
  }

  v4l2_capability cap;
  if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
    if (errno == EINVAL)
      LOG(ERROR) << device_name_ << " is no V4L2 device";
    else
      log_errno("VIDIOC_QUERYCAP failed.");
    ReportFailure();
    return;
  }
  if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
    LOG(ERROR) << device_name_ << " is no video capture device";
    ReportFailure();
    return;
  }
  if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
    LOG(ERROR) << device_name_ << " does not support streaming i/o";
    ReportFailure();
    return;
  }

  gfx::Size frame_size = get_best_frame_size(fd,
                                             V4L2_PIX_FMT_YUYV,
                                             desired_width,
                                             desired_height);
  v4l2_format format = {};
  format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  format.fmt.pix.width = frame_size.width();
  format.fmt.pix.height = frame_size.height();
  format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
  format.fmt.pix.field = V4L2_FIELD_INTERLACED;
  if (xioctl(fd, VIDIOC_S_FMT, &format) == -1) {
    LOG(ERROR) << "VIDIOC_S_FMT failed.";
    ReportFailure();
    return;
  }

  if (!InitializeReadingMode(fd)) {
    ReportFailure();
    return;
  }

  device_descriptor_ = fd;
  frame_width_ = format.fmt.pix.width;
  frame_height_ = format.fmt.pix.height;
  desired_width_ = desired_width;
  desired_height_ = desired_height;
  BrowserThread::PostTask(
      BrowserThread::UI,
      FROM_HERE,
      NewRunnableMethod(this, &Camera::OnInitializeSuccess));
}

void Camera::Uninitialize() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  PostCameraTask(FROM_HERE, NewRunnableMethod(this, &Camera::DoUninitialize));
}

void Camera::DoUninitialize() {
  DCHECK(IsOnCameraThread());
  if (device_descriptor_ == -1) {
    LOG(WARNING) << "Calling uninitialize for uninitialized camera.";
    return;
  }
  DoStopCapturing();
  UnmapVideoBuffers();
  if (close(device_descriptor_) == -1)
    log_errno("Closing the device failed.");
  device_descriptor_ = -1;
}

void Camera::StartCapturing() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  PostCameraTask(FROM_HERE,
                 NewRunnableMethod(this, &Camera::DoStartCapturing));
}

void Camera::DoStartCapturing() {
  DCHECK(IsOnCameraThread());
  if (is_capturing_) {
    LOG(WARNING) << "Capturing is already started.";
    return;
  }

  for (size_t i = 0; i < buffers_.size(); ++i) {
    v4l2_buffer buffer = {};
    buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buffer.memory = V4L2_MEMORY_MMAP;
    buffer.index = i;
    if (xioctl(device_descriptor_, VIDIOC_QBUF, &buffer) == -1) {
      log_errno("VIDIOC_QBUF failed.");
      ReportFailure();
      return;
    }
  }
  v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  if (xioctl(device_descriptor_, VIDIOC_STREAMON, &type) == -1) {
    log_errno("VIDIOC_STREAMON failed.");
    ReportFailure();
    return;
  }
  // No need to post DidProcessCameraThreadMethod() as this method is
  // being posted instead.
  BrowserThread::PostTask(
      BrowserThread::UI,
      FROM_HERE,
      NewRunnableMethod(this,
                        &Camera::OnStartCapturingSuccess));
  is_capturing_ = true;
  PostCameraTask(FROM_HERE,
                 NewRunnableMethod(this, &Camera::OnCapture));
}

void Camera::StopCapturing() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  PostCameraTask(FROM_HERE,
                 NewRunnableMethod(this, &Camera::DoStopCapturing));
}

void Camera::DoStopCapturing() {
  DCHECK(IsOnCameraThread());
  if (!is_capturing_) {
    LOG(WARNING) << "Calling StopCapturing when capturing is not started.";
    return;
  }
  // OnCapture must exit if this flag is not set.
  is_capturing_ = false;
  v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  if (xioctl(device_descriptor_, VIDIOC_STREAMOFF, &type) == -1)
    log_errno("VIDIOC_STREAMOFF failed.");
}

void Camera::GetFrame(SkBitmap* frame) {
  base::AutoLock lock(image_lock_);
  frame->swap(frame_image_);
}

///////////////////////////////////////////////////////////////////////////////
// Camera, private members:

int Camera::OpenDevice(const char* device_name) const {
  DCHECK(IsOnCameraThread());
  struct stat st;
  if (stat(device_name, &st) == -1) {
    log_errno(base::StringPrintf("Cannot identify %s", device_name));
    return -1;
  }
  if (!S_ISCHR(st.st_mode)) {
    LOG(ERROR) << device_name << "is not a device";
    return -1;
  }
  int fd = open(device_name, O_RDWR | O_NONBLOCK, 0);
  if (fd == -1) {
    log_errno(base::StringPrintf("Cannot open %s", device_name));
    return -1;
  }
  return fd;
}

bool Camera::InitializeReadingMode(int fd) {
  DCHECK(IsOnCameraThread());
  v4l2_requestbuffers req;
  req.count = kRequestBuffersCount;
  req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  req.memory = V4L2_MEMORY_MMAP;
  if (xioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
    if (errno == EINVAL)
      LOG(ERROR) << device_name_ << " does not support memory mapping.";
    else
      log_errno("VIDIOC_REQBUFS failed.");
    return false;
  }
  if (req.count < 2U) {
    LOG(ERROR) << "Insufficient buffer memory on " << device_name_;
    return false;
  }
  for (unsigned i = 0; i < req.count; ++i) {
    v4l2_buffer buffer = {};
    buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buffer.memory = V4L2_MEMORY_MMAP;
    buffer.index = i;
    if (xioctl(fd, VIDIOC_QUERYBUF, &buffer) == -1) {
      log_errno("VIDIOC_QUERYBUF failed.");
      return false;
    }
    VideoBuffer video_buffer;
    video_buffer.length = buffer.length;
    video_buffer.start = mmap(
        NULL,  // Start anywhere.
        buffer.length,
        PROT_READ | PROT_WRITE,
        MAP_SHARED,
        fd,
        buffer.m.offset);
    if (video_buffer.start == MAP_FAILED) {
      log_errno("mmap() failed.");
      UnmapVideoBuffers();
      return false;
    }
    buffers_.push_back(video_buffer);
  }
  return true;
}

void Camera::UnmapVideoBuffers() {
  DCHECK(IsOnCameraThread());
  for (size_t i = 0; i < buffers_.size(); ++i) {
    if (munmap(buffers_[i].start, buffers_[i].length) == -1)
      log_errno("munmap failed.");
  }
}

void Camera::OnCapture() {
  DCHECK(IsOnCameraThread());
  if (!is_capturing_)
    return;

  do {
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(device_descriptor_, &fds);

    timeval tv = {};
    tv.tv_sec = kSelectTimeout / base::Time::kMicrosecondsPerSecond;
    tv.tv_usec = kSelectTimeout % base::Time::kMicrosecondsPerSecond;

    int result = select(device_descriptor_ + 1, &fds, NULL, NULL, &tv);
    if (result == -1) {
      if (errno == EINTR)
        continue;
      log_errno("select() failed.");
      ReportFailure();
      break;
    }
    if (result == 0) {
      LOG(ERROR) << "select() timeout.";
      ReportFailure();
      break;
    }
    // EAGAIN - continue select loop.
  } while (!ReadFrame());

  PostCameraTask(FROM_HERE,
                 NewRunnableMethod(this, &Camera::OnCapture));
}

bool Camera::ReadFrame() {
  DCHECK(IsOnCameraThread());
  v4l2_buffer buffer = {};
  buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  buffer.memory = V4L2_MEMORY_MMAP;
  if (xioctl(device_descriptor_, VIDIOC_DQBUF, &buffer) == -1) {
    // Return false only in this case to try again.
    if (errno == EAGAIN)
      return false;

    log_errno("VIDIOC_DQBUF failed.");
    ReportFailure();
    return true;
  }
  if (buffer.index >= buffers_.size()) {
    LOG(ERROR) << "Index of buffer is out of range.";
    ReportFailure();
    return true;
  }
  ProcessImage(buffers_[buffer.index].start);
  if (xioctl(device_descriptor_, VIDIOC_QBUF, &buffer) == -1)
    log_errno("VIDIOC_QBUF failed.");
  return true;
}

void Camera::ProcessImage(void* data) {
  DCHECK(IsOnCameraThread());
  // If desired resolution is higher than available, we crop the available
  // image to get the same aspect ratio and scale the result.
  int desired_width = desired_width_;
  int desired_height = desired_height_;
  if (desired_width > frame_width_ || desired_height > frame_height_) {
    // Compare aspect ratios of the desired and available images.
    // The same as desired_width / desired_height > frame_width / frame_height.
    if (desired_width_ * frame_height_ > frame_width_ * desired_height_) {
      desired_width = frame_width_;
      desired_height = (desired_height_ * frame_width_) / desired_width_;
    } else {
      desired_width = (desired_width_ * frame_height_) / desired_height_;
      desired_height = frame_height_;
    }
  }
  SkBitmap image;
  int crop_left = (frame_width_ - desired_width) / 2;
  int crop_right = frame_width_ - crop_left - desired_width;
  int crop_top = (frame_height_ - desired_height_) / 2;
  image.setConfig(SkBitmap::kARGB_8888_Config, desired_width, desired_height);
  image.allocPixels();
  {
    SkAutoLockPixels lock_image(image);
    // We should reflect the image from the Y axis depending on the value of
    // |mirrored_|. Hence variable increments and origin point.
    int dst_x_origin = 0;
    int dst_x_increment = 1;
    int dst_y_increment = 0;
    if (mirrored_) {
      dst_x_origin = image.width() - 1;
      dst_x_increment = -1;
      dst_y_increment = 2 * image.width();
    }
    uint32_t* dst = image.getAddr32(dst_x_origin, 0);

    uint32_t* src = reinterpret_cast<uint32_t*>(data) +
                    crop_top * (frame_width_ / 2);
    for (int y = 0; y < image.height(); ++y) {
      src += crop_left / 2;
      for (int x = 0; x < image.width(); x += 2) {
        uint32_t yuyv = *src++;
        uint8_t y0 = yuyv & 0xFF;
        uint8_t u = (yuyv >> 8) & 0xFF;
        uint8_t y1 = (yuyv >> 16) & 0xFF;
        uint8_t v = (yuyv >> 24) & 0xFF;
        *dst = convert_yuv_to_rgba(y0, u, v);
        dst += dst_x_increment;
        *dst = convert_yuv_to_rgba(y1, u, v);
        dst += dst_x_increment;
      }
      dst += dst_y_increment;
      src += crop_right / 2;
    }
  }
  if (image.width() < desired_width_ || image.height() < desired_height_) {
    image = skia::ImageOperations::Resize(
        image,
        skia::ImageOperations::RESIZE_LANCZOS3,
        desired_width_,
        desired_height_);
  }
  image.setIsOpaque(true);
  {
    base::AutoLock lock(image_lock_);
    frame_image_.swap(image);
  }
  BrowserThread::PostTask(
      BrowserThread::UI,
      FROM_HERE,
      NewRunnableMethod(this, &Camera::OnCaptureSuccess));
}

void Camera::OnInitializeSuccess() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (delegate_)
    delegate_->OnInitializeSuccess();
}

void Camera::OnInitializeFailure() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (delegate_)
    delegate_->OnInitializeFailure();
}

void Camera::OnStartCapturingSuccess() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (delegate_)
    delegate_->OnStartCapturingSuccess();
}

void Camera::OnStartCapturingFailure() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (delegate_)
    delegate_->OnStartCapturingFailure();
}

void Camera::OnCaptureSuccess() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (delegate_)
    delegate_->OnCaptureSuccess();
}

void Camera::OnCaptureFailure() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (delegate_)
    delegate_->OnCaptureFailure();
}

bool Camera::IsOnCameraThread() const {
  base::AutoLock lock(thread_lock_);
  return thread_ && MessageLoop::current() == thread_->message_loop();
}

void Camera::PostCameraTask(const tracked_objects::Location& from_here,
                            Task* task) {
  base::AutoLock lock(thread_lock_);
  if (!thread_)
    return;
  DCHECK(thread_->IsRunning());
  thread_->message_loop()->PostTask(from_here, task);
}

}  // namespace chromeos