/*
 * Samsung USB FPC1020 driver for libfprint
 *
 * 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 "fpc1020_samsung"

#include "drivers_api.h"

#define FPC1020_EP_OUT     (0x01 | FPI_USB_ENDPOINT_OUT)
#define FPC1020_EP_IN      (0x02 | FPI_USB_ENDPOINT_IN)

#define FPC1020_IMG_W      160
#define FPC1020_IMG_H      160
#define FPC1020_HALF_SIZE  12800
#define FPC1020_XFER_SIZE  12802  // 2-byte header + 12800 pixels

#define FPC1020_TIMEOUT    2000
#define FPC1020_IMG_TIMEOUT 6000 // longer timout for images

#define FPC1020_NUM_INIT_CMDS    16
#define FPC1020_NUM_INIT_PASSES  2
#define FPC1020_POLL_MAX         100

static const guint8 SPI_CONFIG[16] = {
  0xc0, 0xc6, 0x2d, 0x00, 0x08, 0x00, 0xff, 0x01,
  0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// backend device init command
static const guint8 CMD_RESET[]       = { 0xf8 };
static const guint8 CMD_IRQ_READ[]    = { 0x1c, 0x00 };
static const guint8 CMD_HWID[]        = { 0xfc, 0x00, 0x00 };
static const guint8 CMD_CONFIG[]      = { 0x9c, 0x55, 0x40, 0x00, 0x2d, 0x24 };
static const guint8 CMD_REG_6C[]      = { 0x6c, 0x29 };
static const guint8 CMD_REG_90[]      = { 0x90, 0x32 };
static const guint8 CMD_CAL[]         = { 0x68, 0x31, 0x31, 0x31, 0x31,
                                          0x3d, 0x3d, 0x3d, 0x3d };
static const guint8 CMD_CRYPTO[]      = { 0x98, 0x03, 0xeb, 0x3f, 0xb8,
                                          0x0a, 0x07, 0x11, 0x22, 0x6a, 0x5d };
static const guint8 CMD_REG_8C[]      = { 0x8c, 0x12 };
static const guint8 CMD_PXLCTRL[]     = { 0x5c, 0x0b };
static const guint8 CMD_FDET_THRES[]  = { 0xd8, 0x20 };
static const guint8 CMD_FDET_CNTR[]   = { 0xdc, 0x00, 0x0a };
static const guint8 CMD_ADC_GAIN[]    = { 0xa8, 0x0f, 0x0a };
static const guint8 CMD_PXL_OFFSET[]  = { 0xa0, 0x0a, 0x02 };
static const guint8 CMD_FINALIZE[]    = { 0x28 };

struct spi_cmd
{
  const guint8 *data;
  gsize         len;
};

static const struct spi_cmd INIT_CMDS[FPC1020_NUM_INIT_CMDS] = {
  { CMD_RESET,      sizeof (CMD_RESET) },
  { CMD_IRQ_READ,   sizeof (CMD_IRQ_READ) },
  { CMD_IRQ_READ,   sizeof (CMD_IRQ_READ) },
  { CMD_HWID,       sizeof (CMD_HWID) },
  { CMD_CONFIG,     sizeof (CMD_CONFIG) },
  { CMD_REG_6C,     sizeof (CMD_REG_6C) },
  { CMD_REG_90,     sizeof (CMD_REG_90) },
  { CMD_CAL,        sizeof (CMD_CAL) },
  { CMD_CRYPTO,     sizeof (CMD_CRYPTO) },
  { CMD_REG_8C,     sizeof (CMD_REG_8C) },
  { CMD_PXLCTRL,    sizeof (CMD_PXLCTRL) },
  { CMD_FDET_THRES, sizeof (CMD_FDET_THRES) },
  { CMD_FDET_CNTR,  sizeof (CMD_FDET_CNTR) },
  { CMD_ADC_GAIN,   sizeof (CMD_ADC_GAIN) },
  { CMD_PXL_OFFSET, sizeof (CMD_PXL_OFFSET) },
  { CMD_FINALIZE,   sizeof (CMD_FINALIZE) },
};

// full resolution
static const guint8 CMD_FR_OFFSET[]   = { 0xa0, 0x0e, 0x03 };
static const guint8 CMD_FR_SAMPLE[]   = { 0x64, 0x1e };
static const guint8 CMD_FR_PXLCTRL[]  = { 0x5c, 0x0b };
static const guint8 CMD_FR_WINDOW[]   = { 0x54, 0x00, 0xa0, 0x00, 0xa0 };

static const guint8 CMD_ARM[]         = { 0x20 };
static const guint8 CMD_CAPTURE[]     = { 0xc0 };
static const guint8 CMD_FINGER_DET[]  = { 0xd4, 0x00, 0x00, 0x00, 0x00 };

struct _FpDeviceFpc1020Samsung
{
  FpImageDevice parent;

  const guint8 *current_cmd;
  gsize         current_cmd_len;
  guint8        spi_response[16];
  guint8       *top_raw;
  guint8       *bot_raw;
  gint          xfer_complete_count;
  FpiSsm       *capture_ssm;
  gint          init_pass;
  gint          cmd_idx;
  gint          poll_count;
  GSource      *finger_poll_source;
  gboolean      finger_detected;
  gboolean      needs_reinit;
  gboolean      deactivating;
  gboolean      ssm_active;
};

G_DECLARE_FINAL_TYPE (FpDeviceFpc1020Samsung, fpi_device_fpc1020_samsung,
                      FPI, DEVICE_FPC1020_SAMSUNG, FpImageDevice);
G_DEFINE_TYPE (FpDeviceFpc1020Samsung, fpi_device_fpc1020_samsung,
               FP_TYPE_IMAGE_DEVICE);

static void start_finger_poll (FpDeviceFpc1020Samsung *self);
static void start_finger_off_poll (FpDeviceFpc1020Samsung *self);
static void start_capture (FpDeviceFpc1020Samsung *self);

enum spi_cmd_states {
  SPI_CMD_BRIDGE_CONFIG,
  SPI_CMD_BRIDGE_SET_SIZE,
  SPI_CMD_BULK_WRITE,
  SPI_CMD_BULK_READ,
  SPI_CMD_NUM_STATES,
};

static void
spi_read_cb (FpiUsbTransfer *transfer, FpDevice *dev,
             gpointer user_data, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

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

  memset (self->spi_response, 0, sizeof (self->spi_response));
  memcpy (self->spi_response, transfer->buffer,
          MIN ((gsize) transfer->actual_length, sizeof (self->spi_response)));

  fpi_ssm_next_state (transfer->ssm);
}

static void
spi_cmd_run_state (FpiSsm *ssm, FpDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiUsbTransfer *transfer;

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case SPI_CMD_BRIDGE_CONFIG:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xC3, 0x0000, 0x0000,
                                     sizeof (SPI_CONFIG));
      memcpy (transfer->buffer, SPI_CONFIG, sizeof (SPI_CONFIG));
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case SPI_CMD_BRIDGE_SET_SIZE:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xCA, 0x0003,
                                     (guint16) self->current_cmd_len, 0);
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case SPI_CMD_BULK_WRITE:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_bulk_full (transfer, FPC1020_EP_OUT,
                                       g_memdup2 (self->current_cmd,
                                                  self->current_cmd_len),
                                       self->current_cmd_len, g_free);
      transfer->ssm = ssm;
      transfer->short_is_error = TRUE;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case SPI_CMD_BULK_READ:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_bulk (transfer, FPC1020_EP_IN,
                                  self->current_cmd_len);
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               spi_read_cb, NULL);
      break;

    default:
      g_assert_not_reached ();
    }
}

static void
start_spi_cmd_subsm (FpiSsm                 *parent,
                     FpDeviceFpc1020Samsung  *self,
                     const guint8            *cmd,
                     gsize                    len)
{
  FpiSsm *subsm;

  self->current_cmd = cmd;
  self->current_cmd_len = len;

  subsm = fpi_ssm_new (FP_DEVICE (self), spi_cmd_run_state,
                        SPI_CMD_NUM_STATES);
  fpi_ssm_start_subsm (parent, subsm);
}

enum capture_states {
  CAP_CONFIRM_ARM,
  CAP_BWAIT_1,
  CAP_BWAIT_2,
  CAP_BWAIT_3,
  CAP_BWAIT_4,
  CAP_CONFIRM_IRQ,
  CAP_CONFIRM_DETAIL,

  CAP_FR_OFFSET,
  CAP_FR_SAMPLE,
  CAP_FR_PXLCTRL,
  CAP_FR_WINDOW,

  CAP_TRIGGER,
  CAP_POLL_IRQ,
  CAP_POLL_CHECK,
  CAP_POLL_BSTAT,

  CAP_TOP_CONFIG,
  CAP_TOP_SIZE,
  CAP_TOP_XFER,

  CAP_BOT_CONFIG,
  CAP_BOT_SIZE,
  CAP_BOT_XFER,

  CAP_TEARDOWN_1,
  CAP_TEARDOWN_2,
  CAP_TEARDOWN_3,
  CAP_TEARDOWN_4,
  CAP_TEARDOWN_ARM,

  CAP_SUBMIT,

  CAP_NUM_STATES,
};


static void
bstat_delay_cb (FpiUsbTransfer *transfer, FpDevice *dev,
                gpointer user_data, GError *error)
{
  if (error)
    {
      fpi_ssm_mark_failed (transfer->ssm, error);
      return;
    }
  fpi_ssm_next_state_delayed (transfer->ssm, 10);
}

static void
do_bridge_status (FpiSsm *ssm, FpDevice *dev,
                  FpiUsbTransferCallback cb)
{
  FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev);

  fpi_usb_transfer_fill_control (transfer,
                                 G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
                                 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                 G_USB_DEVICE_RECIPIENT_DEVICE,
                                 0xDA, 0x0007, 0x0000, 2);
  transfer->ssm = ssm;
  fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, cb, NULL);
}

static void
poll_bstat_cb (FpiUsbTransfer *transfer, FpDevice *dev,
               gpointer user_data, GError *error)
{
  if (error)
    {
      fpi_ssm_mark_failed (transfer->ssm, error);
      return;
    }
  fpi_ssm_jump_to_state_delayed (transfer->ssm, CAP_POLL_IRQ, 10);
}

enum open_states {
  OPEN_BRIDGE_INIT_1,
  OPEN_BRIDGE_INIT_2,
  OPEN_INIT_CMD,
  OPEN_INIT_NEXT,
  OPEN_NUM_STATES,
};

static void
open_run_state (FpiSsm *ssm, FpDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiUsbTransfer *transfer;

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case OPEN_BRIDGE_INIT_1:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xDB, 0x0006, 0x0000, 0);
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case OPEN_BRIDGE_INIT_2:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xDB, 0x0006, 0x0001, 0);
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case OPEN_INIT_CMD:
      {
        const struct spi_cmd *cmd = &INIT_CMDS[self->cmd_idx];
        start_spi_cmd_subsm (ssm, self, cmd->data, cmd->len);
      }
      break;

    case OPEN_INIT_NEXT:
      self->cmd_idx++;
      if (self->cmd_idx < FPC1020_NUM_INIT_CMDS)
        {
          fpi_ssm_jump_to_state (ssm, OPEN_INIT_CMD);
        }
      else
        {
          self->init_pass++;
          if (self->init_pass < FPC1020_NUM_INIT_PASSES)
            {
              self->cmd_idx = 0;
              fpi_ssm_jump_to_state (ssm, OPEN_INIT_CMD);
            }
          else
            {
              fp_dbg ("FPC1020 init complete (%d passes)",
                      FPC1020_NUM_INIT_PASSES);
              fpi_ssm_mark_completed (ssm);
            }
        }
      break;

    default:
      g_assert_not_reached ();
    }
}

static void
open_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
{
  FpImageDevice *img_dev = FP_IMAGE_DEVICE (dev);

  fpi_image_device_open_complete (img_dev, error);
}

static void
fpc1020_open (FpImageDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiSsm *ssm;
  GError *error = NULL;

  if (!g_usb_device_claim_interface (fpi_device_get_usb_device (FP_DEVICE (dev)),
                                     0, 0, &error))
    {
      fpi_image_device_open_complete (dev, error);
      return;
    }

  self->top_raw = g_malloc0 (FPC1020_XFER_SIZE);
  self->bot_raw = g_malloc0 (FPC1020_XFER_SIZE);
  self->init_pass = 0;
  self->cmd_idx = 0;

  ssm = fpi_ssm_new (FP_DEVICE (dev), open_run_state, OPEN_NUM_STATES);
  fpi_ssm_start (ssm, open_complete);
}

static void
fpc1020_close (FpImageDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  GError *error = NULL;

  g_clear_pointer (&self->top_raw, g_free);
  g_clear_pointer (&self->bot_raw, 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);
}

static void
fpc1020_activate (FpImageDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  self->deactivating = FALSE;
  fpi_image_device_activate_complete (dev, NULL);
}

static void
fpc1020_deactivate (FpImageDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  self->deactivating = TRUE;

  if (self->finger_poll_source)
    {
      g_source_destroy (self->finger_poll_source);
      self->finger_poll_source = NULL;
    }

  if (!self->ssm_active)
    fpi_image_device_deactivate_complete (dev, NULL);
}


enum finger_poll_states {
  FPOLL_READ_IRQ,
  FPOLL_CHECK_IRQ,
  FPOLL_ARM,
  FPOLL_BWAIT_1,
  FPOLL_BWAIT_2,
  FPOLL_BWAIT_3,
  FPOLL_BWAIT_4,
  FPOLL_REREAD_IRQ,
  FPOLL_CHECK_REREAD,
  FPOLL_READ_DETAIL,
  FPOLL_DONE,
  FPOLL_NUM_STATES,
};

static void finger_poll_timer_cb (FpDevice *dev, gpointer user_data);

static void
finger_poll_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  self->ssm_active = FALSE;

  if (self->deactivating)
    {
      fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  if (error)
    {
      fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  if (self->finger_detected)
    {
      fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), TRUE);
    }
  else if (self->needs_reinit)
    {
      self->needs_reinit = FALSE;
      start_finger_poll (self);
    }
  else
    {
      if (!self->deactivating)
        self->finger_poll_source =
          fpi_device_add_timeout (FP_DEVICE (self), 100,
                                  finger_poll_timer_cb, NULL, NULL);
    }
}

static void
finger_poll_run_state (FpiSsm *ssm, FpDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  if (self->deactivating)
    {
      fpi_ssm_mark_completed (ssm);
      return;
    }

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case FPOLL_READ_IRQ:
      start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ));
      break;

    case FPOLL_CHECK_IRQ:
      fp_dbg ("finger poll IRQ: 0x%02x", self->spi_response[1]);
      if (self->spi_response[1] == 0x01)
        {
          fpi_ssm_next_state (ssm);
        }
      else if (self->spi_response[1] & 0x80)
        {
          fpi_ssm_jump_to_state (ssm, FPOLL_READ_DETAIL);
        }
      else
        {
          fpi_ssm_mark_completed (ssm);
        }
      break;

    case FPOLL_ARM:
      start_spi_cmd_subsm (ssm, self, CMD_ARM, sizeof (CMD_ARM));
      break;

    case FPOLL_BWAIT_1:
      do_bridge_status (ssm, dev, bstat_delay_cb);
      break;

    case FPOLL_BWAIT_2:
      do_bridge_status (ssm, dev, bstat_delay_cb);
      break;

    case FPOLL_BWAIT_3:
      do_bridge_status (ssm, dev, bstat_delay_cb);
      break;

    case FPOLL_BWAIT_4:
      do_bridge_status (ssm, dev, bstat_delay_cb);
      break;

    case FPOLL_REREAD_IRQ:
      start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ));
      break;

    case FPOLL_CHECK_REREAD:
      fp_dbg ("finger poll re-read IRQ: 0x%02x", self->spi_response[1]);
      if (self->spi_response[1] & 0x80)
        {
          fpi_ssm_next_state (ssm);
        }
      else
        {
          fp_dbg ("ARM consumed state, will reinit");
          self->needs_reinit = TRUE;
          fpi_ssm_mark_completed (ssm);
        }
      break;

    case FPOLL_READ_DETAIL:
      start_spi_cmd_subsm (ssm, self, CMD_FINGER_DET,
                           sizeof (CMD_FINGER_DET));
      break;

    case FPOLL_DONE:
      self->finger_detected = TRUE;
      fpi_ssm_mark_completed (ssm);
      break;

    default:
      g_assert_not_reached ();
    }
}

static void
finger_poll_timer_cb (FpDevice *dev, gpointer user_data)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiSsm *ssm;

  self->finger_poll_source = NULL;

  if (self->deactivating)
    {
      fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), NULL);
      return;
    }

  self->finger_detected = FALSE;
  ssm = fpi_ssm_new (dev, finger_poll_run_state, FPOLL_NUM_STATES);
  self->ssm_active = TRUE;
  fpi_ssm_start (ssm, finger_poll_complete);
}

enum reinit_states {
  REINIT_CMD,
  REINIT_NEXT,
  REINIT_NUM_STATES,
};

static void
reinit_run_state (FpiSsm *ssm, FpDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  if (self->deactivating)
    {
      fpi_ssm_mark_completed (ssm);
      return;
    }

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case REINIT_CMD:
      {
        const struct spi_cmd *cmd = &INIT_CMDS[self->cmd_idx];
        start_spi_cmd_subsm (ssm, self, cmd->data, cmd->len);
      }
      break;

    case REINIT_NEXT:
      self->cmd_idx++;
      if (self->cmd_idx < FPC1020_NUM_INIT_CMDS)
        {
          fpi_ssm_jump_to_state (ssm, REINIT_CMD);
        }
      else
        {
          self->init_pass++;
          if (self->init_pass < FPC1020_NUM_INIT_PASSES)
            {
              self->cmd_idx = 0;
              fpi_ssm_jump_to_state (ssm, REINIT_CMD);
            }
          else
            {
              fp_dbg ("FPC1020 re-init complete");
              fpi_ssm_mark_completed (ssm);
            }
        }
      break;

    default:
      g_assert_not_reached ();
    }
}

static void
reinit_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  self->ssm_active = FALSE;

  if (self->deactivating)
    {
      fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  if (error)
    {
      fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  self->finger_poll_source =
    fpi_device_add_timeout (FP_DEVICE (self), 100,
                            finger_poll_timer_cb, NULL, NULL);
}

static void
start_finger_poll (FpDeviceFpc1020Samsung *self)
{
  FpiSsm *ssm;

  if (self->deactivating)
    return;

  self->init_pass = 0;
  self->cmd_idx = 0;

  ssm = fpi_ssm_new (FP_DEVICE (self), reinit_run_state, REINIT_NUM_STATES);
  self->ssm_active = TRUE;
  fpi_ssm_start (ssm, reinit_complete);
}

static void
img_xfer_check_done (FpDeviceFpc1020Samsung *self)
{
  if (g_atomic_int_add (&self->xfer_complete_count, 1) + 1 >= 2)
    fpi_ssm_next_state (self->capture_ssm);
}

static void
img_in_cb (FpiUsbTransfer *transfer, FpDevice *dev,
           gpointer user_data, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  guint8 *dest = user_data;

  if (error)
    {
      fpi_ssm_mark_failed (self->capture_ssm, error);
      return;
    }

  memcpy (dest, transfer->buffer,
          MIN ((gsize) transfer->actual_length, FPC1020_XFER_SIZE));
  fp_dbg ("img IN: %ld bytes", (long) transfer->actual_length);

  img_xfer_check_done (self);
}

static void
img_out_cb (FpiUsbTransfer *transfer, FpDevice *dev,
            gpointer user_data, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  if (error)
    {
      fpi_ssm_mark_failed (self->capture_ssm, error);
      return;
    }

  fp_dbg ("img OUT: %ld bytes", (long) transfer->actual_length);
  img_xfer_check_done (self);
}

static void
submit_concurrent_image_xfer (FpiSsm *ssm, FpDevice *dev, guint8 *dest)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiUsbTransfer *in_xfer;
  FpiUsbTransfer *out_xfer;
  guint8 *out_buf;

  g_atomic_int_set (&self->xfer_complete_count, 0);
  self->capture_ssm = ssm;

  /* Submit bulk IN (doesnt work if not ordered? bridge req.) */
  in_xfer = fpi_usb_transfer_new (dev);
  fpi_usb_transfer_fill_bulk (in_xfer, FPC1020_EP_IN, FPC1020_XFER_SIZE);
  fpi_usb_transfer_submit (in_xfer, FPC1020_IMG_TIMEOUT, NULL,
                           img_in_cb, dest);

  /* Submit bulk OUT */
  out_buf = g_malloc0 (FPC1020_XFER_SIZE);
  out_buf[0] = 0xC4;

  out_xfer = fpi_usb_transfer_new (dev);
  fpi_usb_transfer_fill_bulk_full (out_xfer, FPC1020_EP_OUT,
                                   out_buf, FPC1020_XFER_SIZE, g_free);
  out_xfer->short_is_error = TRUE;
  fpi_usb_transfer_submit (out_xfer, FPC1020_IMG_TIMEOUT, NULL,
                           img_out_cb, NULL);
}

static void
capture_run_state (FpiSsm *ssm, FpDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiUsbTransfer *transfer;

  if (self->deactivating)
    {
      fpi_ssm_mark_completed (ssm);
      return;
    }

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case CAP_CONFIRM_ARM:
      start_spi_cmd_subsm (ssm, self, CMD_ARM, sizeof (CMD_ARM));
      break;

    case CAP_BWAIT_1:
    case CAP_BWAIT_2:
    case CAP_BWAIT_3:
    case CAP_BWAIT_4:
      do_bridge_status (ssm, dev, bstat_delay_cb);
      break;

    case CAP_CONFIRM_IRQ:
      start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ));
      break;

    case CAP_CONFIRM_DETAIL:
      start_spi_cmd_subsm (ssm, self, CMD_FINGER_DET,
                           sizeof (CMD_FINGER_DET));
      break;

    case CAP_FR_OFFSET:
      start_spi_cmd_subsm (ssm, self, CMD_FR_OFFSET,
                           sizeof (CMD_FR_OFFSET));
      break;

    case CAP_FR_SAMPLE:
      start_spi_cmd_subsm (ssm, self, CMD_FR_SAMPLE,
                           sizeof (CMD_FR_SAMPLE));
      break;

    case CAP_FR_PXLCTRL:
      start_spi_cmd_subsm (ssm, self, CMD_FR_PXLCTRL,
                           sizeof (CMD_FR_PXLCTRL));
      break;

    case CAP_FR_WINDOW:
      start_spi_cmd_subsm (ssm, self, CMD_FR_WINDOW,
                           sizeof (CMD_FR_WINDOW));
      break;

    case CAP_TRIGGER:
      self->poll_count = 0;
      start_spi_cmd_subsm (ssm, self, CMD_CAPTURE, sizeof (CMD_CAPTURE));
      break;

    case CAP_POLL_IRQ:
      start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ));
      break;

    case CAP_POLL_CHECK:
      if (self->spi_response[1] == 0x20)
        {
          fp_dbg ("Capture ready after %d polls", self->poll_count + 1);
          fpi_ssm_jump_to_state (ssm, CAP_TOP_CONFIG);
        }
      else
        {
          self->poll_count++;
          if (self->poll_count >= FPC1020_POLL_MAX)
            {
              fpi_ssm_mark_failed (ssm,
                fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
                                          "Capture not ready after %d polls",
                                          FPC1020_POLL_MAX));
              return;
            }
          fpi_ssm_next_state (ssm);
        }
      break;

    case CAP_POLL_BSTAT:
      {
        FpiUsbTransfer *xfer = fpi_usb_transfer_new (dev);
        fpi_usb_transfer_fill_control (xfer,
                                       G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
                                       G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                       G_USB_DEVICE_RECIPIENT_DEVICE,
                                       0xDA, 0x0007, 0x0000, 2);
        xfer->ssm = ssm;
        fpi_usb_transfer_submit (xfer, FPC1020_TIMEOUT, NULL,
                                 poll_bstat_cb, NULL);
      }
      break;

    case CAP_TOP_CONFIG:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xC3, 0x0000, 0x0000,
                                     sizeof (SPI_CONFIG));
      memcpy (transfer->buffer, SPI_CONFIG, sizeof (SPI_CONFIG));
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case CAP_TOP_SIZE:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xCA, 0x0003,
                                     (guint16) FPC1020_XFER_SIZE, 0);
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case CAP_TOP_XFER:
      submit_concurrent_image_xfer (ssm, dev, self->top_raw);
      break;

    case CAP_BOT_CONFIG:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xC3, 0x0000, 0x0000,
                                     sizeof (SPI_CONFIG));
      memcpy (transfer->buffer, SPI_CONFIG, sizeof (SPI_CONFIG));
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case CAP_BOT_SIZE:
      transfer = fpi_usb_transfer_new (dev);
      fpi_usb_transfer_fill_control (transfer,
                                     G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
                                     G_USB_DEVICE_REQUEST_TYPE_VENDOR,
                                     G_USB_DEVICE_RECIPIENT_DEVICE,
                                     0xCA, 0x0003,
                                     (guint16) FPC1020_XFER_SIZE, 0);
      transfer->ssm = ssm;
      fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL,
                               fpi_ssm_usb_transfer_cb, NULL);
      break;

    case CAP_BOT_XFER:
      submit_concurrent_image_xfer (ssm, dev, self->bot_raw);
      break;

    /* ── Teardown ── */
    case CAP_TEARDOWN_1:
      start_spi_cmd_subsm (ssm, self, CMD_ADC_GAIN, sizeof (CMD_ADC_GAIN));
      break;

    case CAP_TEARDOWN_2:
      start_spi_cmd_subsm (ssm, self, CMD_PXL_OFFSET,
                           sizeof (CMD_PXL_OFFSET));
      break;

    case CAP_TEARDOWN_3:
      start_spi_cmd_subsm (ssm, self, CMD_ADC_GAIN, sizeof (CMD_ADC_GAIN));
      break;

    case CAP_TEARDOWN_4:
      start_spi_cmd_subsm (ssm, self, CMD_PXL_OFFSET,
                           sizeof (CMD_PXL_OFFSET));
      break;

    case CAP_TEARDOWN_ARM:
      start_spi_cmd_subsm (ssm, self, CMD_ARM, sizeof (CMD_ARM));
      break;

    case CAP_SUBMIT:
      {
        FpImage *img = fp_image_new (FPC1020_IMG_W, FPC1020_IMG_H);

        memcpy (img->data, self->top_raw + 2, FPC1020_HALF_SIZE);
        memcpy (img->data + FPC1020_HALF_SIZE, self->bot_raw + 2,
                FPC1020_HALF_SIZE);

        /* Needs invert and reverse. */
        img->flags = FPI_IMAGE_COLORS_INVERTED | FPI_IMAGE_PARTIAL;
        img->ppmm = 20.0;

        {
          guint8 pmin = 255, pmax = 0;
          gulong psum = 0;
          int i;
          for (i = 0; i < FPC1020_IMG_W * FPC1020_IMG_H; i++)
            {
              guint8 v = img->data[i];
              if (v < pmin) pmin = v;
              if (v > pmax) pmax = v;
              psum += v;
            }
          fp_dbg ("Image stats: min=%u max=%u mean=%lu hdr_top=%02x%02x hdr_bot=%02x%02x",
                  pmin, pmax, psum / (FPC1020_IMG_W * FPC1020_IMG_H),
                  self->top_raw[0], self->top_raw[1],
                  self->bot_raw[0], self->bot_raw[1]);
        }

        fp_dbg ("Image captured (%dx%d)", FPC1020_IMG_W, FPC1020_IMG_H);
        fpi_image_device_image_captured (FP_IMAGE_DEVICE (dev), img);
        fpi_ssm_mark_completed (ssm);
      }
      break;

    default:
      g_assert_not_reached ();
    }
}

static void
capture_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  self->ssm_active = FALSE;
  self->capture_ssm = NULL;

  if (self->deactivating)
    {
      fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  if (error)
    fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error);
}

static void
start_capture (FpDeviceFpc1020Samsung *self)
{
  FpiSsm *ssm;

  ssm = fpi_ssm_new (FP_DEVICE (self), capture_run_state, CAP_NUM_STATES);
  self->ssm_active = TRUE;
  self->capture_ssm = ssm;
  fpi_ssm_start (ssm, capture_complete);
}

enum finger_off_states {
  FOFF_READ_IRQ,
  FOFF_CHECK,
  FOFF_NUM_STATES,
};

static void finger_off_poll_complete (FpiSsm *ssm, FpDevice *dev,
                                      GError *error);

static void
finger_off_run_state (FpiSsm *ssm, FpDevice *dev)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  if (self->deactivating)
    {
      fpi_ssm_mark_completed (ssm);
      return;
    }

  switch (fpi_ssm_get_cur_state (ssm))
    {
    case FOFF_READ_IRQ:
      start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ));
      break;

    case FOFF_CHECK:
      fp_dbg ("finger off IRQ: 0x%02x", self->spi_response[1]);
      if (self->spi_response[1] == 0x00)
        {
          fpi_ssm_mark_completed (ssm);
        }
      else
        {
          fpi_ssm_mark_completed (ssm);
        }
      break;

    default:
      g_assert_not_reached ();
    }
}

static void finger_off_timer_cb (FpDevice *dev, gpointer user_data);

static void
finger_off_poll_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  self->ssm_active = FALSE;

  if (self->deactivating)
    {
      fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  if (error)
    {
      fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error);
      return;
    }

  if (self->spi_response[1] == 0x00)
    {
      fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE);
    }
  else
    {
      start_finger_off_poll (self);
    }
}

static void
finger_off_timer_cb (FpDevice *dev, gpointer user_data)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);
  FpiSsm *ssm;

  self->finger_poll_source = NULL;

  if (self->deactivating)
    {
      fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), NULL);
      return;
    }

  ssm = fpi_ssm_new (dev, finger_off_run_state, FOFF_NUM_STATES);
  self->ssm_active = TRUE;
  fpi_ssm_start (ssm, finger_off_poll_complete);
}

static void
start_finger_off_poll (FpDeviceFpc1020Samsung *self)
{
  if (self->deactivating)
    return;

  self->finger_poll_source =
    fpi_device_add_timeout (FP_DEVICE (self), 100,
                            finger_off_timer_cb, NULL, NULL);
}


static void
fpc1020_change_state (FpImageDevice *dev, FpiImageDeviceState state)
{
  FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev);

  switch (state)
    {
    case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON:
      start_finger_poll (self);
      break;

    case FPI_IMAGE_DEVICE_STATE_CAPTURE:
      start_capture (self);
      break;

    case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF:
      start_finger_off_poll (self);
      break;

    case FPI_IMAGE_DEVICE_STATE_INACTIVE:
    case FPI_IMAGE_DEVICE_STATE_ACTIVATING:
    case FPI_IMAGE_DEVICE_STATE_DEACTIVATING:
    case FPI_IMAGE_DEVICE_STATE_IDLE:
      break;
    }
}


static const FpIdEntry id_table[] = {
  { .vid = 0x04e8, .pid = 0x7301 },
  { .vid = 0,      .pid = 0 },
};

static void
fpi_device_fpc1020_samsung_init (FpDeviceFpc1020Samsung *self)
{
}

static void
fpi_device_fpc1020_samsung_class_init (FpDeviceFpc1020SamsungClass *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 = "FPC1020 (Samsung USB-SPI Bridge)";
  dev_class->type = FP_DEVICE_TYPE_USB;
  dev_class->id_table = id_table;
  dev_class->scan_type = FP_SCAN_TYPE_PRESS;

  img_class->img_width = FPC1020_IMG_W;
  img_class->img_height = FPC1020_IMG_H;
  img_class->bz3_threshold = 12;

  img_class->img_open = fpc1020_open;
  img_class->img_close = fpc1020_close;
  img_class->activate = fpc1020_activate;
  img_class->deactivate = fpc1020_deactivate;
  img_class->change_state = fpc1020_change_state;
}
