/*
 * Next Biometrics driver for libfprint
 *
 * Copyright (C) 2021 Huan Wang <fredwanghuan@gmail.com>
 * Copyright (C) 2011-2012 Andrej Krutak <dev@andree.sk>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define FP_COMPONENT "nb1010"
#include "fpi-log.h"

#include "drivers_api.h"

#define FRAME_HEIGHT 180
#define FRAME_WIDTH 256

#define NB1010_EP_OUT 0x02 | FPI_USB_ENDPOINT_OUT
#define NB1010_EP_IN 0x03 | FPI_USB_ENDPOINT_IN

#define NB1010_SENSITIVITY_BIT 12

#define NB1010_CMD_RECV_LEN 16
#define NB1010_CAPTURE_RECV_LEN 540
#define NB1010_CAPTURE_HEADER_LEN 25

#define NB1010_LINE_PER_PARTIAL 2
#define NB1010_N_PARTIAL (FRAME_HEIGHT / NB1010_LINE_PER_PARTIAL)

#define NB1010_DEFAULT_TIMEOUT 500
#define NB1010_TRANSITION_DELAY 50

/* Loop ssm states */
enum {
  M_WAIT_PRINT,
  M_REQUEST_PRINT,
  M_CHECK_PRINT,
  M_READ_PRINT_PRESTART,
  M_READ_PRINT_START,
  M_READ_PRINT_POLL,
  M_SUBMIT_PRINT,

  /* Number of states */
  M_LOOP_NUM_STATES,
};

/*
 * The Follow Commands are obtained by decoding the usbcap, so it does not expose all the command available to the device.
 * Known:
 * 1. every command starts with 0x80
 * 2. second byte is the comand, third byte is the seqence nubmer, init with rand, gets incremented
 *    everytime a new instruction is sent to the device. However device does not care or check the sequence, just echo back
 *    whatever chosen by the host.
 * 3. cmd: 0x07 check, expect [0x80, 0x29...] as response
 * 4. cmd: 0x16 ???, expect [0x80, 0x20...] as response. Happens during device init.
 * 5. cmd: 0x13 print device, expect [0x80, 0x23...] as response. Response contains the device string
 * 6. cmd: 0x38 check finger, expect [0x80, 0x37...] as response. The 14th byte indicate whether finger present [0-255]
 * 7. cmd: 0x0d ???, expect [0x80, 0x20...] as response. Happens before capture.
 * 8. cmd: 0x12 capture, expect [0x80, 0x20...] as response. After capture read 90 times in sequence to get all the frame.
 */

static guint8 nb1010_cmd_check_finger[] = {
  0x80, 0x38, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00,
};

/* pre capture, dont know what does it do, but appears everytime a capture begins */
static guint8 nb1010_cmd_precapture[] = {
  0x80, 0x0d, 0x03, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
};

static guint8 nb1010_cmd_capture[] = {
  0x80, 0x12, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00,
  0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
};

struct _FpiDeviceNb1010
{
  FpImageDevice parent;
  FpiSsm       *ssm;
  guint8       *scanline_buf;
  gboolean      deactivating;
  int           partial_received;
};
G_DECLARE_FINAL_TYPE (FpiDeviceNb1010, fpi_device_nb1010, FPI, DEVICE_NB1010, FpImageDevice);
G_DEFINE_TYPE (FpiDeviceNb1010, fpi_device_nb1010, FP_TYPE_IMAGE_DEVICE);

static void
nb1010_dev_init (FpImageDevice *dev)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);
  GError *error = NULL;

  g_usb_device_claim_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), 0, 0, &error);

  self->scanline_buf = g_malloc0 (FRAME_WIDTH * FRAME_HEIGHT);

  fpi_image_device_open_complete (dev, error);
  fp_dbg ("nb1010 Initialized");
}

static void
nb1010_dev_deinit (FpImageDevice *dev)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);
  GError *error = NULL;

  g_clear_pointer (&self->scanline_buf, g_free);

  g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), 0, 0, &error);
  fpi_image_device_close_complete (dev, error);
  fp_dbg ("nb1010 Deinitialized");
}

static void
nb1010_dev_activate (FpImageDevice *dev)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);

  self->deactivating = FALSE;

  fpi_image_device_activate_complete (dev, NULL);
  fp_dbg ("nb1010 Activated");
}

static void
nb1010_dev_deactivated (FpImageDevice *dev, GError * err)
{
  fpi_image_device_deactivate_complete (dev, err);
  fp_dbg ("nb1010 Deactivated");
}

static void
nb1010_dev_deactivate (FpImageDevice *dev)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);

  self->deactivating = TRUE;
  if (self->ssm == NULL)
    nb1010_dev_deactivated (dev, NULL);
}

static void
nb1010_request_fingerprint (FpiDeviceNb1010 *dev)
{
  FpiUsbTransfer *transfer = NULL;

  transfer = fpi_usb_transfer_new (FP_DEVICE ( dev));
  transfer->short_is_error = TRUE;
  transfer->ssm = dev->ssm;

  fpi_usb_transfer_fill_bulk_full (transfer, NB1010_EP_OUT,
                                   nb1010_cmd_check_finger, G_N_ELEMENTS (nb1010_cmd_check_finger),
                                   NULL);
  fpi_usb_transfer_submit (transfer, NB1010_DEFAULT_TIMEOUT,
                           fpi_device_get_cancellable (FP_DEVICE (dev)),
                           fpi_ssm_usb_transfer_cb, NULL);
}

static void
nb1010_check_fingerprint_cb (FpiUsbTransfer *transfer, FpDevice *dev,
                             gpointer unused_data, GError *error)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);

  if (error)
    {
      fpi_ssm_mark_failed (transfer->ssm, error);
      return;
    }
  if (self->deactivating)
    {
      fpi_ssm_mark_completed (transfer->ssm);
      return;
    }

  if (transfer->buffer[NB1010_SENSITIVITY_BIT] > 0x30)
    fpi_ssm_next_state (transfer->ssm);
  else
    fpi_ssm_jump_to_state (transfer->ssm, M_WAIT_PRINT);
}

static void
nb1010_cmd_check_fingerprint (FpiDeviceNb1010 *dev)
{
  FpiUsbTransfer *transfer = NULL;

  transfer = fpi_usb_transfer_new (FP_DEVICE ( dev));
  transfer->short_is_error = TRUE;
  transfer->ssm = dev->ssm;

  fpi_usb_transfer_fill_bulk (transfer, NB1010_EP_IN, NB1010_CMD_RECV_LEN);
  fpi_usb_transfer_submit (transfer, NB1010_DEFAULT_TIMEOUT,
                           fpi_device_get_cancellable (FP_DEVICE (dev)),
                           nb1010_check_fingerprint_cb, NULL);
}

static void
nb1010_read_ignore_data_cb (FpiUsbTransfer *transfer, FpDevice *dev,
                            gpointer unused_data, GError *error)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);
  FpiUsbTransfer *new_transfer = NULL;

  if (error)
    {
      fpi_ssm_mark_failed (transfer->ssm, error);
      return;
    }
  if (self->deactivating)
    {
      fpi_ssm_mark_completed (transfer->ssm);
      return;
    }

  new_transfer = fpi_usb_transfer_new ( dev );
  new_transfer->short_is_error = TRUE;
  new_transfer->ssm = transfer->ssm;

  fpi_usb_transfer_fill_bulk (new_transfer, NB1010_EP_IN, NB1010_CMD_RECV_LEN);
  fpi_usb_transfer_submit (new_transfer, NB1010_DEFAULT_TIMEOUT,
                           fpi_device_get_cancellable (FP_DEVICE (dev)),
                           fpi_ssm_usb_transfer_cb, NULL);
}

static void
nb1010_write_ignore_read (FpiDeviceNb1010 *dev, guint8 *buf, gsize len)
{
  FpiUsbTransfer *transfer = NULL;

  transfer = fpi_usb_transfer_new (FP_DEVICE ( dev));
  transfer->short_is_error = TRUE;
  transfer->ssm = dev->ssm;

  fpi_usb_transfer_fill_bulk_full (transfer, NB1010_EP_OUT, buf, len, NULL);
  fpi_usb_transfer_submit (transfer, NB1010_DEFAULT_TIMEOUT,
                           fpi_device_get_cancellable (FP_DEVICE (dev)),
                           nb1010_read_ignore_data_cb, NULL);
}


static void
nb1010_read_capture_cb (FpiUsbTransfer *transfer, FpDevice *dev,
                        gpointer unused_data, GError *error)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);

  if (error)
    {
      fpi_ssm_mark_failed (transfer->ssm, error);
      return;
    }
  if (self->deactivating)
    {
      fpi_ssm_mark_completed (transfer->ssm);
      return;
    }

  g_assert (transfer->actual_length == NB1010_CAPTURE_RECV_LEN);

  size_t offset = self->partial_received * NB1010_LINE_PER_PARTIAL * FRAME_WIDTH;

  memcpy (self->scanline_buf + offset,
          transfer->buffer + NB1010_CAPTURE_HEADER_LEN, NB1010_LINE_PER_PARTIAL * FRAME_WIDTH);

  self->partial_received++;
  if (self->partial_received == NB1010_N_PARTIAL)
    {
      fpi_ssm_next_state (transfer->ssm);
      return;
    }

  fpi_usb_transfer_submit (fpi_usb_transfer_ref (transfer), NB1010_DEFAULT_TIMEOUT,
                           fpi_device_get_cancellable (FP_DEVICE (dev)),
                           nb1010_read_capture_cb, NULL);
}

static void
nb1010_read_capture (FpiDeviceNb1010 *dev)
{
  FpiUsbTransfer *transfer = NULL;

  transfer = fpi_usb_transfer_new ( FP_DEVICE ( dev));
  transfer->short_is_error = TRUE;
  transfer->ssm = dev->ssm;

  fpi_usb_transfer_fill_bulk (transfer, NB1010_EP_IN, NB1010_CAPTURE_RECV_LEN);
  fpi_usb_transfer_submit (transfer, NB1010_DEFAULT_TIMEOUT,
                           fpi_device_get_cancellable (FP_DEVICE (dev)),
                           nb1010_read_capture_cb, NULL);
}

static int
submit_image (FpiSsm        *ssm,
              FpImageDevice *dev)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);
  FpImage *img;

  img = fp_image_new (FRAME_WIDTH, FRAME_HEIGHT);
  if (img == NULL)
    return 0;

  memcpy (img->data, self->scanline_buf, FRAME_WIDTH * FRAME_HEIGHT);
  fpi_image_device_image_captured (dev, img);

  return 1;
}

static void
m_loop_complete (FpiSsm *ssm, FpDevice *_dev, GError *error)
{
  fp_dbg ("nb1010 ssm complete cb");

  FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (_dev);

  self->ssm = NULL;

  if (self->deactivating)
    nb1010_dev_deactivated (dev, error);
  else if (error != NULL)
    fpi_image_device_session_error (dev, error);
}

static void
m_loop_state (FpiSsm *ssm, FpDevice *_dev)
{
  FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (_dev);

  if (self->deactivating)
    {
      fp_dbg ("deactivating, marking completed");
      fpi_ssm_mark_completed (ssm);
      return;
    }

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case M_WAIT_PRINT:
      /* Wait fingerprint scanning */
      fpi_ssm_next_state_delayed (ssm, NB1010_TRANSITION_DELAY);
      break;

    case M_REQUEST_PRINT:
      nb1010_request_fingerprint (self);
      break;

    case M_CHECK_PRINT:
      nb1010_cmd_check_fingerprint (self);
      break;

    case M_READ_PRINT_PRESTART:
      fpi_image_device_report_finger_status (dev, TRUE);
      nb1010_write_ignore_read (self, nb1010_cmd_precapture, G_N_ELEMENTS (nb1010_cmd_precapture));
      break;

    case M_READ_PRINT_START:
      self->partial_received = 0;
      nb1010_write_ignore_read (self, nb1010_cmd_capture, G_N_ELEMENTS (nb1010_cmd_capture));
      break;

    case M_READ_PRINT_POLL:
      nb1010_read_capture (self);
      break;

    case M_SUBMIT_PRINT:
      if (submit_image (ssm, dev))
        {
          fpi_ssm_mark_completed (ssm);
          fpi_image_device_report_finger_status (dev, FALSE);
        }
      else
        {
          fpi_ssm_jump_to_state (ssm, M_WAIT_PRINT);
        }
      break;

    default:
      g_assert_not_reached ();
    }
}

static void
nb1010_dev_change_state (FpImageDevice *dev, FpiImageDeviceState state)
{
  FpiDeviceNb1010 *self = FPI_DEVICE_NB1010 (dev);
  FpiSsm *ssm_loop;

  if (state == FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON)
    {
      ssm_loop = fpi_ssm_new (FP_DEVICE (dev), m_loop_state, M_LOOP_NUM_STATES);
      self->ssm = ssm_loop;
      fpi_ssm_start (ssm_loop, m_loop_complete);
    }
}


static const FpIdEntry id_table[] = {
  { .vid = 0x298d,  .pid = 0x1010, },
  { .vid = 0,  .pid = 0,  .driver_data = 0 },
};

static void
fpi_device_nb1010_init (FpiDeviceNb1010 *self)
{
}

static void
fpi_device_nb1010_class_init (FpiDeviceNb1010Class *klass)
{
  FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass);
  FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass);

  dev_class->id = FP_COMPONENT;
  dev_class->full_name = "NextBiometrics NB-1010-U";
  dev_class->type = FP_DEVICE_TYPE_USB;
  dev_class->id_table = id_table;
  dev_class->scan_type = FP_SCAN_TYPE_PRESS;

  img_class->img_height = FRAME_HEIGHT;
  img_class->img_width = FRAME_WIDTH;

  img_class->bz3_threshold = 24;

  img_class->img_open = nb1010_dev_init;
  img_class->img_close = nb1010_dev_deinit;
  img_class->activate = nb1010_dev_activate;
  img_class->deactivate = nb1010_dev_deactivate;
  img_class->change_state = nb1010_dev_change_state;
}
