/**
 * Description of the Primax PagePartner model
 */
static P5_Model pagepartner_model = {
  "Primax PagePartner",
  "Primax",
  "PagePartner",
  SANE_I18N ("sheetfed scanner"),

  {300, 200, 150, 100, 0},
  /* 500 seems also possible */
  {600, 400, 300, 200, 150, 100, 0},

  300,
  600,
  100,
  100,
  16,

  SANE_FIX (0.0),
  SANE_FIX (0.0),
  SANE_FIX (215.9),
  SANE_FIX (300.0),
};

#ifdef HAVE_LINUX_PPDEV_H
static char *
addr_name (uint16_t addr)
{
  switch (addr)
    {
    case DATA:
      return "DATA";
      break;
    case STATUS:
      return "STATUS";
      break;
    case CONTROL:
      return "CONTROL";
      break;
    case EPPADR:
      return "EPPADR";
      break;
    case EPPDATA:
      return "EPPDATA";
      break;
    default:
      return "*ERROR*";
    }
}
#endif

/** @brief low level hardware access functions
 * @{
 */

static uint8_t
p5_inb (int fd, uint16_t addr)
{
#ifdef HAVE_LINUX_PPDEV_H
  uint8_t val = 0xff;
  int rc, mode = 0xff;

  switch (addr)
    {
    case DATA:
      rc = ioctl (fd, PPRDATA, &val);
      break;
    case STATUS:
      rc = ioctl (fd, PPRSTATUS, &val);
      break;
    case CONTROL:
      rc = ioctl (fd, PPRCONTROL, &val);
      break;
    case EPPDATA:
      mode = 1;			/* data_reverse */
      rc = ioctl (fd, PPDATADIR, &mode);
      mode = IEEE1284_MODE_EPP | IEEE1284_DATA;
      rc = ioctl (fd, PPSETMODE, &mode);
#ifdef PPSETFLAGS
      mode = PP_FASTREAD;
      rc = ioctl (fd, PPSETFLAGS, &mode);
#endif
      rc = read (fd, &val, 1);
      break;
    default:
      DBG (DBG_error, "p5_inb(%s) escaped ppdev\n", addr_name (addr));
      return 0xFF;
    }
  if (rc < 0)
    {
      DBG (DBG_error, "ppdev ioctl returned <%s>\n", strerror (errno));
    }
  return val;
#else
  if(fd && addr)
    return 0;
  return 0;
#endif
}

static void
p5_outb (int fd, uint16_t addr, uint8_t value)
{
#ifdef HAVE_LINUX_PPDEV_H
  int rc = 0, mode = 0xff;

  switch (addr)
    {
    case DATA:
      rc = ioctl (fd, PPWDATA, &value);
      break;
    case CONTROL:
      mode = value & 0x20;
      rc = ioctl (fd, PPDATADIR, &mode);
      if (!rc)
	{
	  value = value & 0xDF;
	  rc = ioctl (fd, PPWCONTROL, &value);
	}
      break;
    case EPPDATA:
      mode = 0;			/* data forward */
      rc = ioctl (fd, PPDATADIR, &mode);
      mode = IEEE1284_MODE_EPP | IEEE1284_DATA;
      rc = ioctl (fd, PPSETMODE, &mode);
      rc = write (fd, &value, 1);
      break;
    case EPPADR:
      mode = 0;			/* data forward */
      rc = ioctl (fd, PPDATADIR, &mode);
      mode = IEEE1284_MODE_EPP | IEEE1284_ADDR;
      rc = ioctl (fd, PPSETMODE, &mode);
      rc = write (fd, &value, 1);
      break;
    default:
      DBG (DBG_error, "p5_outb(%s,0x%02x) escaped ppdev\n", addr_name (addr),
	   value);
      break;
    }
  if (rc < 0)
    {
      DBG (DBG_error, "ppdev ioctl returned <%s>\n", strerror (errno));
    }
#else
  if(fd && addr && value)
    return;
#endif /* HAVE_LINUX_PPDEV_H */
}

static void
write_reg (int fd, uint8_t index, uint8_t value)
{
  uint8_t idx;

  /* both nibbles hold the same value */
  idx = index & 0x0F;
  DBG (DBG_io2, "write_reg(REG%X,0x%x)\n", idx, value);
  idx = idx << 4 | idx;
  p5_outb (fd, EPPADR, idx);
  p5_outb (fd, EPPDATA, value);
}

static uint8_t
read_reg (int fd, uint8_t index)
{
  uint8_t idx;

  /* both nibbles hold the same value */
  idx = index & 0x0F;
  idx = idx << 4 | idx;
  p5_outb (fd, EPPADR, idx);
  return p5_inb (fd, EPPDATA);
}

#ifdef HAVE_LINUX_PPDEV_H
static int
read_data (int fd, uint8_t * data, int length)
{
  int mode, rc, nb;
  unsigned char bval;

  bval = REG8;
  mode = IEEE1284_MODE_EPP | IEEE1284_ADDR;
  rc = ioctl (fd, PPSETMODE, &mode);
  rc = write (fd, &bval, 1);

  mode = 1;			/* data_reverse */
  rc = ioctl (fd, PPDATADIR, &mode);
#ifdef PPSETFLAGS
  mode = PP_FASTREAD;
  rc = ioctl (fd, PPSETFLAGS, &mode);
#endif
  mode = IEEE1284_MODE_EPP | IEEE1284_DATA;
  rc = ioctl (fd, PPSETMODE, &mode);
  nb = 0;
  while (nb < length)
    {
      rc = read (fd, data + nb, length - nb);
      if (rc < 0)
	{
	  DBG (DBG_error, "memtest: error reading data back!\n");
	  return 0;
	}
      else
	{
	  nb += rc;
	}
    }

  return 1;
}

static void
index_write_data (int fd, uint8_t index, uint8_t * data, int length)
{
  int mode;
  unsigned char bval;

  bval = index;
  mode = IEEE1284_MODE_EPP | IEEE1284_ADDR;
  ioctl (fd, PPSETMODE, &mode);
  write (fd, &bval, 1);

  mode = IEEE1284_MODE_EPP | IEEE1284_DATA;
  ioctl (fd, PPSETMODE, &mode);
  mode = 0;			/* data forward */
  ioctl (fd, PPDATADIR, &mode);
  write (fd, data, length);
  return;
}

static void
write_data (int fd, uint8_t * data, int length)
{
  index_write_data (fd, REG8, data, length);
}

static void
write_reg2 (int fd, uint8_t index, uint16_t value)
{
  uint8_t data2[2];

  data2[0] = value & 0xff;
  data2[1] = value >> 8;
  index_write_data (fd, index, data2, 2);
}
#else

static int
read_data (int fd, uint8_t * data, int length)
{
  if(fd && data && length)
    return -1;
  return -1;
}

static void
write_data (int fd, uint8_t * data, int length)
{
  if(fd && data && length)
    return;
}

static void
write_reg2 (int fd, uint8_t index, uint16_t value)
{
  if(fd && index && value)
    return;
}
#endif

/**
 * @}
 */


/** @brief This function checks a memory buffer.
 * This function writes at the given memory address then read it back
 * to check the scanner is correctly working.
 * @param fd file descriptor used to access hardware
 * @param addr address where to write and read
 * @return SANE_TRUE on success, SANE_FALSE otherwise
 */
static int
memtest (int fd, uint16_t addr)
{
  uint8_t sent[256];
  uint8_t back[256];
  int i;

  write_reg2 (fd, REG1, addr);
  for (i = 0; i < 256; i++)
    {
      sent[i] = (uint8_t) i;
      back[i] = 0;
    }
  write_data (fd, sent, 256);
  read_data (fd, back, 256);

  /* check if data read back is the same that the one sent */
  for (i = 0; i < 256; i++)
    {
      if (back[i] != sent[i])
	{
	  return SANE_FALSE;
	}
    }

  return SANE_TRUE;
}


#define P5_INB(k,y,z) val=p5_inb(k,y); if(val!=z) { DBG(DBG_error,"expected 0x%02x, got 0x%02x\n",z, val); return SANE_FALSE; }

/** @brief connect to scanner
 * This function sends the connect sequence for the scanner.
 * @param fd filedescriptor of the parallel port communication channel
 * @return SANE_TRUE in case of success, SANE_FALSE otherwise
 */
static int
connect (int fd)
{
  uint8_t val;

  p5_inb (fd, CONTROL);
  p5_outb (fd, CONTROL, 0x04);
  p5_outb (fd, DATA, 0x02);
  P5_INB (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x03);
  P5_INB (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x03);
  p5_outb (fd, DATA, 0x83);
  P5_INB (fd, DATA, 0x83);
  p5_outb (fd, DATA, 0x82);
  P5_INB (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0x02);
  p5_outb (fd, DATA, 0x82);
  p5_outb (fd, DATA, 0xFF);
  DBG (DBG_info, "connect() OK...\n");
  return SANE_TRUE;
}

static int
disconnect (int fd)
{
  uint8_t val;

  p5_outb (fd, CONTROL, 0x04);
  p5_outb (fd, DATA, 0x00);
  P5_INB (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x01);
  P5_INB (fd, DATA, 0x01);
  p5_outb (fd, DATA, 0x01);
  p5_outb (fd, DATA, 0x81);
  p5_outb (fd, DATA, 0x01);
  p5_outb (fd, DATA, 0x81);
  P5_INB (fd, DATA, 0x81);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x81);
  P5_INB (fd, DATA, 0x81);
  p5_outb (fd, DATA, 0x01);
  p5_outb (fd, DATA, 0x81);
  p5_outb (fd, DATA, 0x01);
  p5_outb (fd, DATA, 0x81);
  P5_INB (fd, DATA, 0x81);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  P5_INB (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_outb (fd, DATA, 0x00);
  p5_outb (fd, DATA, 0x80);
  p5_inb (fd, CONTROL);
  p5_outb (fd, CONTROL, 0x0C);

  return SANE_STATUS_GOOD;
}

static void
setadresses (int fd, uint16_t start, uint16_t end)
{
  write_reg (fd, REG3, start & 0xff);
  write_reg (fd, REG4, start >> 8);
  write_reg (fd, REG5, end & 0xff);
  write_reg (fd, REG6, end >> 8);
  DBG (DBG_io, "setadresses(0x%x,0x%x); OK...\n", start, end);
}

#ifdef HAVE_LINUX_PPDEV_H
/** @brief open parallel port device
 * opens parallel port's low level device in EPP mode
 * @param devicename nam of the real device or the special value 'auto'
 * @return file descriptor in cas of successn -1 otherwise
 */
static int
open_pp (const char *devicename)
{
  int fd, mode = 0;
  char *name;

  DBG (DBG_proc, "open_pp: start, devicename=%s\n", devicename);
  /* TODO improve auto device finding */
  if (strncmp (devicename, "auto", 4) == 0)
    {
      name = strdup("/dev/parport0");
    }
  else
    {
      name = strdup(devicename);
    }

  /* open device */
  fd = open (name, O_RDWR);
  if (fd < 0)
    {
      switch (errno)
	{
	case ENOENT:
#ifdef ENIO
	case ENXIO:
#endif
#ifdef ENODEV
	case ENODEV:
#endif
	  DBG (DBG_error, "open_pp: no %s device ...\n", name);
	  break;
	case EACCES:
	  DBG (DBG_error,
	       "open_pp: current user cannot use existing %s device ...\n",
	       name);
	  break;
	default:
	  DBG (DBG_error, "open_pp: %s while opening %s\n", strerror (errno),
	       name);
	}
      return -1;
    }
  free(name);

  /* claim device and set it to EPP */
  ioctl (fd, PPCLAIM);
  ioctl (fd, PPGETMODES, &mode);
  if (mode & PARPORT_MODE_PCSPP)
    DBG (DBG_io, "PARPORT_MODE_PCSPP\n");
  if (mode & PARPORT_MODE_TRISTATE)
    DBG (DBG_io, "PARPORT_MODE_TRISTATE\n");
  if (mode & PARPORT_MODE_EPP)
    DBG (DBG_io, "PARPORT_MODE_EPP\n");
  if (mode & PARPORT_MODE_ECP)
    DBG (DBG_io, "PARPORT_MODE_ECP\n");
  if (mode & PARPORT_MODE_COMPAT)
    DBG (DBG_io, "PARPORT_MODE_COMPAT\n");
  if (mode & PARPORT_MODE_DMA)
    DBG (DBG_io, "PARPORT_MODE_DMA\n");
  if (mode & PARPORT_MODE_EPP)
    {
      mode = IEEE1284_MODE_EPP;
    }
  else
    {
      /*
         if (mode & PARPORT_MODE_ECP)
         {
         mode = IEEE1284_MODE_ECP;
         }
         else
       */
      {
	mode = -1;
      }
    }
  if (mode == -1)
    {
      DBG (DBG_error, "open_pp: no EPP mode, giving up ...\n");
      ioctl (fd, PPRELEASE);
      close (fd);
      return -1;
    }
  ioctl (fd, PPNEGOT, &mode);
  ioctl (fd, PPSETMODE, &mode);
  DBG (DBG_proc, "open_pp: exit\n");
  return fd;
}

/** close low level device
 * release and close low level hardware device
 */
static void
close_pp (int fd)
{
  int mode = IEEE1284_MODE_COMPAT;

  if (fd > 2)
    {
      ioctl (fd, PPNEGOT, &mode);
      ioctl (fd, PPRELEASE);
      close (fd);
    }
}

#else /*  HAVE_LINUX_PPDEV_H */

static int
open_pp (const char *devicename)
{
  if(devicename)
    return -1;
  return -1;
}

static void
close_pp (int fd)
{
  if(fd)
    return;
}
#endif /*  HAVE_LINUX_PPDEV_H */

/** @brief test if a document is inserted
 * Test if a document is inserted by reading register E
 * @param fd file descriptor to access scanner
 * @return SANE_STATUS_NO_DOCS if no document or SANE_STATUS_GOOD
 * if something is present.
 */
static SANE_Status
test_document (int fd)
{
  int detector;

  /* check for document presence 0xC6: present, 0xC3 no document */
  detector = read_reg (fd, REGE);
  DBG (DBG_io, "test_document: detector=0x%02X\n", detector);

  /* document inserted */
  if (detector & 0x04)
    return SANE_STATUS_GOOD;

  return SANE_STATUS_NO_DOCS;
}

/**
 * return the amount of scanned data available
 * @param fd file descriptor to access scanner
 * @return available byte number
 */
static int
available_bytes (int fd)
{
  int counter;

  /* read the number of 256 bytes block of scanned data */
  counter = read_reg (fd, REG9);
  DBG (DBG_io, "available_bytes: available_bytes=0x%02X\n", counter);
  return 256 * counter;
}

static SANE_Status
build_correction (P5_Device * dev, unsigned int dpi, unsigned int mode,
		  unsigned int start, unsigned int width)
{
  unsigned int i, j, shift, step;

  DBG (DBG_proc, "build_correction: start=%d, width=%d\n", start, width);
  DBG (DBG_trace, "build_correction: dpi=%d, mode=%d\n", dpi, mode);

  /* loop on calibration data to find the matching one */
  j = 0;
  while (dev->calibration_data[j]->dpi != dpi)
    {
      j++;
      if (j > MAX_RESOLUTIONS)
	{
	  DBG (DBG_error, "build_correction: couldn't find calibration!\n");
	  return SANE_STATUS_INVAL;
	}
    }

  if (dev->gain != NULL)
    {
      free (dev->gain);
      dev->gain = NULL;
    }
  if (dev->offset != NULL)
    {
      free (dev->offset);
      dev->offset = NULL;
    }
  dev->gain = (float *) malloc (width * sizeof (float));
  if (dev->gain == NULL)
    {
      DBG (DBG_error,
	   "build_correction: failed to allocate memory for gain!\n");
      return SANE_STATUS_NO_MEM;
    }
  dev->offset = (uint8_t *) malloc (width);
  if (dev->offset == NULL)
    {
      DBG (DBG_error,
	   "build_correction: failed to allocate memory for offset!\n");
      return SANE_STATUS_NO_MEM;
    }

  /* compute starting point of calibration data to use */
  shift = start;
  step = 1;
  if (mode == MODE_GRAY)
    {
      /* we use green data */
      shift += 1;
      step = 3;
    }
  for (i = 0; i < width; i += step)
    {
      if (dev->calibration_data[j]->white_data[shift + i] -
	  dev->calibration_data[0]->black_data[shift + i] > BLACK_LEVEL)
	{
	  dev->gain[i] =
	    WHITE_TARGET /
	    ((float)
	     (dev->calibration_data[j]->white_data[shift + i] -
	      dev->calibration_data[j]->black_data[shift + i]));
	  dev->offset[i] = dev->calibration_data[j]->black_data[shift + i];
	}
      else
	{
	  dev->gain[i] = 1.0;
	  dev->offset[i] = 0;
	}
    }
  return SANE_STATUS_GOOD;
  DBG (DBG_proc, "build_correction: end\n");
}

/** @brief start up a real scan
 * This function starts the scan with the given parameters.
 * @param dev device describing hardware
 * @param mode color, gray level or lineart.
 * @param dpi desired scan resolution.
 * @param startx coordinate of the first pixel to scan in
 * scan's resolution coordinate
 * @param width width of the scanned area
 * scanner's physical scan aread.
 * @return SANE_STATUS_GOOD if scan is successfully started
 */
static SANE_Status
start_scan (P5_Device * dev, int mode, unsigned int dpi, unsigned int startx,
	    unsigned int width)
{
  uint8_t reg0=0;
  uint8_t reg2=0;
  uint8_t regF=0;
  uint16_t addr=0;
  uint16_t start, end;
  unsigned int xdpi;

  DBG (DBG_proc, "start_scan: start \n");
  DBG (DBG_io, "start_scan: startx=%d, width=%d, dpi=%d\n", startx, width,
       dpi);

	/** @brief register values
	 * - reg2 : reg2 seems related to x dpi and provides only 100,
	 *   150, 200 and 300 resolutions.
	 * - regF : lower nibble gives y dpi resolution ranging from 150
	 *   to 1200 dpi.
	 */
  xdpi = dpi;
  switch (dpi)
    {
    case 100:
      reg2 = 0x90;
      regF = 0xA2;
      break;
    case 150:
      reg2 = 0x10;
      regF = 0xA4;
      break;
    case 200:
      reg2 = 0x80;
      regF = 0xA6;
      break;
    case 300:
      reg2 = 0x00;
      regF = 0xA8;
      break;
    case 400:
      reg2 = 0x80;		/* xdpi=200 */
      regF = 0xAA;
      xdpi = 200;
      break;
    case 500:
      reg2 = 0x00;
      regF = 0xAC;
      xdpi = 300;
      break;
    case 600:
      reg2 = 0x00;
      regF = 0xAE;
      xdpi = 300;
      break;
    }

  switch (mode)
    {
    case MODE_COLOR:
      reg0 = 0x00;
      addr = 0x0100;
      break;
    case MODE_GRAY:
      /* green channel only */
      reg0 = 0x20;
      addr = 0x0100;
      break;
    case MODE_LINEART:
      reg0 = 0x40;
      addr = 0x0908;
      break;
    }

  write_reg (dev->fd, REG1, 0x01);
  write_reg (dev->fd, REG7, 0x00);
  write_reg (dev->fd, REG0, reg0);
  write_reg (dev->fd, REG1, 0x00);
  write_reg (dev->fd, REGF, regF);
  /* the memory addr used to test need not to be related
   * to resolution, 0x0100 could be always used */
  /* TODO get rid of it */
  memtest (dev->fd, addr);

  /* handle case where dpi>xdpi */
  start = startx;
  if (dpi > xdpi)
    {
      width = (width * xdpi) / dpi;
      start = (startx * xdpi) / dpi;
    }

  /* compute and set start addr */
  if (mode == MODE_COLOR)
    {
      start = start * 3;
      width = width * 3;
    }
  end = start + width + 1;

  /* build calibration data for the scan */
  if (dev->calibrated)
    {
      build_correction (dev, xdpi, mode, start, width);
    }

  setadresses (dev->fd, start, end);

  write_reg (dev->fd, REG1, addr >> 8);
  write_reg (dev->fd, REG2, reg2);
  regF = (regF & 0x0F) | 0x80;
  write_reg (dev->fd, REGF, regF);
  write_reg (dev->fd, REG0, reg0);
  if (mode == MODE_LINEART)
    {
      write_reg (dev->fd, 0x07, 0x04);
    }
  else
    {
      write_reg (dev->fd, 0x07, 0x00);
    }
  write_reg (dev->fd, REG1, addr >> 8);
  write_reg2 (dev->fd, REG1, addr);
  write_reg (dev->fd, REGF, regF | 0x01);
  write_reg (dev->fd, REG0, reg0 | 0x0C);
  if (mode == MODE_LINEART)
    {
      write_reg (dev->fd, REG1, 0x19);
    }
  else
    {
      write_reg (dev->fd, REG1, 0x11);
    }
  DBG (DBG_proc, "start_scan: exit\n");
  return SANE_STATUS_GOOD;
}

/** read a line of scan data
 * @param dev device to read
 * @param data pointer where to store data
 * @param length total bytes to read on one line
 * @param ltr total number of lines to read
 * @param retry signals that the function must read as much lines it can
 * @param x2 tells that lines must be enlarged by a 2 factor
 * @param mode COLOR_MODE if color mode
 * @returns number of data lines read, -1 in case of error
 */
static int
read_line (P5_Device * dev, uint8_t * data, size_t length, int ltr,
	   SANE_Bool retry, SANE_Bool x2, int mode, SANE_Bool correction)
{
  uint8_t counter, read, cnt;
  uint8_t inbuffer[MAX_SENSOR_PIXELS * 2 * 3 + 2];
  unsigned int i, factor;
  float val;

  DBG (DBG_proc, "read_line: trying to read %d lines of %lu bytes\n", ltr,
       (unsigned long)length);

  counter = read_reg (dev->fd, REG9);
  DBG (DBG_io, "read_line: %d bytes available\n", counter * 256);
  read = 0;
  if (x2 == SANE_FALSE)
    {
      factor = 1;
    }
  else
    {
      factor = 2;
    }

  /* in retry mode we read until not enough data, but in no retry
   * read only one line , counter give us 256 bytes block available
   * and we want an number multiple of color channels */
  cnt = (255 + length / factor) / 256;
  while ((counter > cnt && retry == 1) || (counter > cnt && read == 0))
    {
      /* read data from scanner, first and last byte aren't picture data */
      read_data (dev->fd, inbuffer, length / factor + 2);

      /* image correction */
      if (correction == SANE_TRUE)
	{
	  for (i = 0; i < length / factor; i++)
	    {
	      val = inbuffer[i + 1] - dev->offset[i];
	      if (val > 0)
		{
		  val = val * dev->gain[i];
		  if (val < 255)
		    inbuffer[i + 1] = val;
		  else
		    inbuffer[i + 1] = 255;
		}
	      else
		{
		  inbuffer[i + 1] = 0;
		}
	    }
	}

      /* handle horizontal data doubling */
      if (x2 == SANE_FALSE)
	{
	  memcpy (data + read * length, inbuffer + 1, length);
	}
      else
	{
	  if (mode == MODE_COLOR)
	    {
	      for (i = 0; i < length / factor; i += 3)
		{
		  data[read * length + i * factor] = inbuffer[i + 1];
		  data[read * length + i * factor + 1] = inbuffer[i + 2];
		  data[read * length + i * factor + 2] = inbuffer[i + 3];
		  data[read * length + i * factor + 3] = inbuffer[i + 1];
		  data[read * length + i * factor + 4] = inbuffer[i + 2];
		  data[read * length + i * factor + 5] = inbuffer[i + 3];
		}
	    }
	  else
	    {
	      for (i = 0; i < length / factor; i++)
		{
		  data[read * length + i * factor] = inbuffer[i + 1];
		  data[read * length + i * factor + 1] = inbuffer[i + 1];
		}
	    }
	}
      read++;
      if (retry == SANE_TRUE)
	{
	  read_reg (dev->fd, REGF);
	  read_reg (dev->fd, REGA);
	  read_reg (dev->fd, REG9);
	  counter = read_reg (dev->fd, REG9);
	  read_reg (dev->fd, REGA);
	  if (read >= ltr)
	    {
	      DBG (DBG_io, "read_line returning %d lines\n", read);
	      return read;
	    }
	  counter = read_reg (dev->fd, REG9);
	}
    }
  read_reg (dev->fd, REGF);
  read_reg (dev->fd, REGA);
  read_reg (dev->fd, REG9);
  counter = read_reg (dev->fd, REG9);
  read_reg (dev->fd, REGA);
  DBG (DBG_io, "read_line returning %d lines\n", read);
  return read;
}


static SANE_Status
eject (int fd)
{
  int detector;

  DBG (DBG_proc, "eject: start ...\n");

  do
    {
      write_reg2 (fd, REG1, 0x1110);
      detector = read_reg (fd, REGE);
      detector = read_reg (fd, REGE);
    }
  while ((detector & 0x04) != 0);
  write_reg (fd, REG0, 0x00);
  write_reg (fd, REG1, 0x00);
  write_reg (fd, REGF, 0x82);
  write_reg (fd, REG7, 0x00);

  DBG (DBG_proc, "eject: end.\n");
  return SANE_STATUS_GOOD;
}

/** @brief wait for document to be present in feeder
 * Polls document sensor until something is present. Give up after 20 seconds
 * @param fd file descriptor of the physical device
 */
/* static int
wait_document (int fd, uint8_t detector)
{
  int count = 0;
  uint8_t val;

  write_reg (fd, REG1, 0x00);
  write_reg (fd, REG7, 0x00);
  detector = read_reg (fd, REGE);
  while (detector == 0xc3 && count < 20)
    {
      sleep (1);
      count++;
      detector = read_reg (fd, REGE);
    }
  setadresses (fd, 0x002d, 0x09c7);
  write_reg (fd, REG1, 0x00);
  write_reg (fd, REG2, 0x90);
  write_reg (fd, REGF, 0x82);
  write_reg (fd, REG0, 0x00);
  val = p5_inb (fd, STATUS) & 0xf8;
  if (val != 0xf8)
    {
      DBG (DBG_error, "wait_document: unexpected STATUS value 0x%02x instead of 0xf8", val);
    }
  if (count >= 20)
    {
      DBG (DBG_error, "wait_document: failed to detect document!\n");
      return 0;
    }
  return 1;
} */

/** @brief move at 150 dpi
 * move the paper at 150 dpi motor speed by the amount specified
 * @params dev pointer to the device structure
 */
static SANE_Status
move (P5_Device * dev)
{
  int skip, done, read, count;
  SANE_Status status = SANE_STATUS_GOOD;
  unsigned char buffer[256];

  DBG (DBG_proc, "move: start\n");

  /* compute number of lines to skip */
  skip = dev->ystart;

  /* works, but remains to be explained ... */
  if (dev->ydpi > 300)
    skip = skip / 2;

  DBG (DBG_io, "move: skipping %d lines at %d dpi\n", skip, dev->ydpi);

  /* we do a real scan of small width, discarding data */
  done = 0;
  status = start_scan (dev, MODE_GRAY, dev->ydpi, 0, 256);
  if (status != SANE_STATUS_GOOD)
    {
      DBG (DBG_error, "move: failed to start scan\n");
      return SANE_STATUS_INVAL;
    }

  do
    {
      /* test if document left the feeder */
      status = test_document (dev->fd);
      if (status == SANE_STATUS_NO_DOCS)
	{
	  DBG (DBG_info,
	       "move: document was shorter than the required move\n");
	  return SANE_STATUS_INVAL;
	}
      /* test if data is available */
      count = available_bytes (dev->fd);
      if (count)
	{
	  read =
	    read_line (dev, buffer, 256, 1, SANE_FALSE, SANE_FALSE,
		       MODE_GRAY, SANE_FALSE);
	  if (read == -1)
	    {
	      DBG (DBG_error, "move: failed to read data\n");
	      return SANE_STATUS_INVAL;
	    }
	  done += read;
	}
    }
  while (done < skip);

  /* reset scanner */
  write_reg2 (dev->fd, REG1, 0x1110);
  count = read_reg (dev->fd, REGE);
  count = read_reg (dev->fd, REGE);
  write_reg (dev->fd, REG0, 0x00);
  write_reg (dev->fd, REG1, 0x00);
  write_reg (dev->fd, REGF, 0x82);
  write_reg (dev->fd, REG7, 0x00);

  DBG (DBG_proc, "move: exit\n");
  return status;
}

/** clean up calibration data
 * @param dev device to clean up
 */
static void
cleanup_calibration (P5_Device * dev)
{
  int i;

  for (i = 0; i < MAX_RESOLUTIONS * 2; i++)
    {
      if (dev->calibration_data[i] != NULL)
	{
	  free (dev->calibration_data[i]);
	  dev->calibration_data[i] = NULL;
	}
    }
  dev->calibrated = SANE_FALSE;
}

/** detect a black scan line
 * parses the given buffer and return SANE_TRUE if the line is an
 * acceptable black line for calibration
 * @param buffer data line to parse
 * @param pixels number of pixels
 * @param mode MODE_COLOR or MODE_GRAY
 * @returns SANE_TRUE if it is considered as a white line
 */
static SANE_Bool
is_black_line (uint8_t * buffer, unsigned int pixels, int mode)
{
  unsigned int i, start, end, count, width;

  /* compute width in bytes */
  if (mode == MODE_COLOR)
    {
      width = pixels * 3;
    }
  else
    {
      width = pixels;
    }

  /* we allow the calibration target to be narrower than full width, ie
   * black margin at both ends of the line */
  start = (5 * width) / 100;
  end = (95 * width) / 100;
  count = 0;

  /* count number of black bytes */
  for (i = start; i < end; i++)
    {
      if (buffer[i] > BLACK_LEVEL)
	{
	  count++;
	}
    }

  /* we allow 3% black pixels maximum */
  if (count > (3 * width) / 100)
    {
      DBG (DBG_io, "is_black_line=SANE_FALSE\n");
      return SANE_FALSE;
    }

  DBG (DBG_io, "is_black_line=SANE_TRUE\n");
  return SANE_TRUE;
}

/** detect a white scan line
 * parses the given buffer and return SANE_TRUE if the line is an
 * acceptable white line for calibration
 * @param buffer data line to parse
 * @param pixels number of pixels
 * @param mode MODE_COLOR or MODE_GRAY
 * @returns SANE_TRUE if it is considered as a white line
 */
static SANE_Bool
is_white_line (uint8_t * buffer, unsigned int pixels, int mode)
{
  unsigned int i, start, end, count, width;

  /* compute width in bytes */
  if (mode == MODE_COLOR)
    {
      width = pixels * 3;
    }
  else
    {
      width = pixels;
    }

  /* we allow the calibration target to be narrower than full width, ie
   * black margin at both ends of the line */
  start = (5 * width) / 100;
  end = (95 * width) / 100;
  count = 0;

  /* count number of black bytes */
  for (i = start; i < end; i++)
    {
      if (buffer[i] < BLACK_LEVEL)
	{
	  count++;
	}
    }

  /* we allow 3% black pixels maximum */
  if (count > (3 * width) / 100)
    {
      DBG (DBG_io, "is_white_line=SANE_FALSE\n");
      return SANE_FALSE;
    }

  DBG (DBG_io, "is_white_line=SANE_TRUE\n");
  return SANE_TRUE;
}

/* ------------------------------------------------------------------------- */
/* writes gray data to a pnm file */
/*
static void
write_gray_data (unsigned char *image, char *name, SANE_Int width,
		 SANE_Int height)
{
  FILE *fdbg = NULL;

  fdbg = fopen (name, "wb");
  if (fdbg == NULL)
    return;
  fprintf (fdbg, "P5\n%d %d\n255\n", width, height);
  fwrite (image, width, height, fdbg);
  fclose (fdbg);
}
*/

/* ------------------------------------------------------------------------- */
/* writes rgb data to a pnm file */
static void
write_rgb_data (char *name, unsigned char *image, SANE_Int width,
		SANE_Int height)
{
  FILE *fdbg = NULL;

  fdbg = fopen (name, "wb");
  if (fdbg == NULL)
    return;
  fprintf (fdbg, "P6\n%d %d\n255\n", width, height);
  fwrite (image, width * 3, height, fdbg);
  fclose (fdbg);
}

/** give calibration file name
 * computes the calibration file name to use based on the
 * backend name and device
 */
static char *
calibration_file (const char *devicename)
{
  char *ptr = NULL;
  char tmp_str[PATH_MAX];

  ptr = getenv ("HOME");
  if (ptr != NULL)
    {
      sprintf (tmp_str, "%s/.sane/p5-%s.cal", ptr, devicename);
    }
  else
    {
      ptr = getenv ("TMPDIR");
      if (ptr != NULL)
	{
	  sprintf (tmp_str, "%s/p5-%s.cal", ptr, devicename);
	}
      else
	{
	  sprintf (tmp_str, "/tmp/p5-%s.cal", devicename);
	}
    }
  DBG (DBG_trace, "calibration_file: using >%s< for calibration file name\n",
       tmp_str);
  return strdup (tmp_str);
}

/** restore calibration data
 * restore calibration data by loading previously saved calibration data
 * @param dev device to restore
 * @return SANE_STATUS_GOOD on success, otherwise error code
 */
static SANE_Status
restore_calibration (P5_Device * dev)
{
  SANE_Status status = SANE_STATUS_GOOD;
  char *fname = NULL;
  FILE *fcalib = NULL;
  size_t size;
  int i;

  DBG (DBG_proc, "restore_calibration: start\n");
  cleanup_calibration (dev);
  fname = calibration_file (dev->model->name);
  fcalib = fopen (fname, "rb");
  if (fcalib == NULL)
    {
      DBG (DBG_error, "restore_calibration: failed to open %s!\n", fname);
      free (fname);
      return SANE_STATUS_IO_ERROR;
    }

  /* loop filling calibration data until EOF reached */
  i = 0;
  while (!feof (fcalib) && (i < 2 * MAX_RESOLUTIONS))
    {
      dev->calibration_data[i] = malloc (sizeof (P5_Calibration_Data));
      if (dev->calibration_data[i] == NULL)
	{
	  cleanup_calibration (dev);
	  free (fname);
	  fclose (fcalib);
	  DBG (DBG_error,
	       "restore_calibration: failed to allocate memory for calibration\n");
	  return SANE_STATUS_NO_MEM;
	}
      size =
	fread (dev->calibration_data[i], 1, sizeof (P5_Calibration_Data),
	       fcalib);
      if (feof (fcalib))
	{
	  free (dev->calibration_data[i]);
	  dev->calibration_data[i] = NULL;
	}
      else if (size != sizeof (P5_Calibration_Data))
	{
	  cleanup_calibration (dev);
	  free (fname);
	  fclose (fcalib);
	  DBG (DBG_error, "restore_calibration: failed to read from file\n");
	  return SANE_STATUS_IO_ERROR;
	}
      DBG (DBG_trace,
	   "restore_calibration: read 1 calibration structure from file\n");
      i++;
    }

  dev->calibrated = SANE_TRUE;
  fclose (fcalib);
  free (fname);

  DBG (DBG_proc, "restore_calibration: end\n");
  return status;
}

/** save calibration data
 * save calibration data from memory to file
 * @param dev device calibration to save
 * @return SANE_STATUS_GOOD on success, otherwise error code
 */
static SANE_Status
save_calibration (P5_Device * dev)
{
  SANE_Status status = SANE_STATUS_GOOD;
  char *fname = NULL;
  FILE *fcalib = NULL;
  int i;
  size_t size;

  DBG (DBG_proc, "save_calibration: start\n");
  fname = calibration_file (dev->model->name);
  fcalib = fopen (fname, "wb");
  if (fcalib == NULL)
    {
      DBG (DBG_error, "save_calibration: failed to open %s!\n", fname);
      free (fname);
      return SANE_STATUS_IO_ERROR;
    }

  /* loop filling calibration data until EOF reached */
  i = 0;
  while (dev->calibration_data[i] != NULL && (i < 2 * MAX_RESOLUTIONS))
    {
      size =
	fwrite (dev->calibration_data[i], sizeof (P5_Calibration_Data), 1,
		fcalib);
      if (size != sizeof (P5_Calibration_Data))
	{
	  free (fname);
	  fclose (fcalib);
	  DBG (DBG_error, "save_calibration: failed to write to file\n");
	  return SANE_STATUS_IO_ERROR;
	}
      DBG (DBG_trace,
	   "save_calibration: wrote 1 calibration structure to file\n");
      i++;
    }

  fclose (fcalib);
  free (fname);

  DBG (DBG_proc, "save_calibration: end\n");
  return status;
}

/** calibrate scanner
 * calibrates scanner by scanning a white sheet to get
 * reference data. The black reference data is extracted from the lines
 * that precede the physical document.
 * Calibration is done at 300 color, then data is built for other modes
 * and resolutions.
 * @param dev device to calibrate
 */
static SANE_Status
sheetfed_calibration (P5_Device * dev)
{
  uint8_t buffer[MAX_SENSOR_PIXELS * 3];
  uint16_t white_data[MAX_SENSOR_PIXELS * 3];
  uint16_t black_data[MAX_SENSOR_PIXELS * 3];
  unsigned int i, j, k, dpi, pixels, read, black, white;
  float coeff;
  unsigned int red, green, blue;
  int line;
  SANE_Status status;
  char title[40];

  FILE *dbg = fopen ("debug.pnm", "wb");
  fprintf (dbg, "P6\n%d %d\n255\n", MAX_SENSOR_PIXELS,
	   CALIBRATION_SKIP_LINES * 4);

  DBG (DBG_proc, "sheetfed_calibration: start\n");

  /* check calibration target has been loaded in ADF */
  status = test_document (dev->fd);
  if (status == SANE_STATUS_NO_DOCS)
    {
      DBG (DBG_error,
	   "sheetfed_calibration: no calibration target present!\n");
      return SANE_STATUS_NO_DOCS;
    }

  /* clean up calibration data */
  cleanup_calibration (dev);

  /*  a RGB scan to get reference data */
  /* initialize calibration slot for the resolution */
  i = 0;
  dpi = dev->model->max_xdpi;
  pixels = MAX_SENSOR_PIXELS;
  dev->calibration_data[i] =
    (P5_Calibration_Data *) malloc (sizeof (P5_Calibration_Data));
  if (dev->calibration_data[i] == NULL)
    {
      cleanup_calibration (dev);
      DBG (DBG_error,
	   "sheetfed_calibration: failed to allocate memory for calibration\n");
      return SANE_STATUS_NO_MEM;
    }
  dev->calibration_data[i]->dpi = dpi;

  /* start scan */
  status = start_scan (dev, MODE_COLOR, dpi, 0, pixels);
  if (status != SANE_STATUS_GOOD)
    {
      cleanup_calibration (dev);
      DBG (DBG_error,
	   "sheetfed_calibration: failed to start scan at %d dpi\n", dpi);
      return SANE_STATUS_INVAL;
    }

  white = 0;
  black = 0;
  read = 0;
  for (j = 0; j < pixels * 3; j++)
    {
      black_data[j] = 0;
      white_data[j] = 0;
    }

  /* read lines and gather black and white ones until enough for sensor's
   * native resolution */
  do
    {
      status = test_document (dev->fd);
      if (status == SANE_STATUS_NO_DOCS && (white < 10 || black < 10))
	{
	  cleanup_calibration (dev);
	  DBG (DBG_error,
	       "sheetfed_calibration: calibration sheet too short!\n");
	  return SANE_STATUS_INVAL;
	}
      memset (buffer, 0x00, MAX_SENSOR_PIXELS * 3);
      line =
	read_line (dev, buffer, pixels * 3, 1, SANE_FALSE, SANE_FALSE,
		   MODE_COLOR, SANE_FALSE);
      if (line == -1)
	{
	  DBG (DBG_error, "sheetfed_calibration: failed to read data\n");
	  return SANE_STATUS_INVAL;
	}

      /* if a data line has been read, add it to reference data */
      if (line)
	{
	  read++;
	  fwrite (buffer, pixels * 3, 1, dbg);
	  if (is_white_line (buffer, pixels, MODE_COLOR) && white < 256)
	    {
	      white++;
	      /* first calibration lines are skipped */
	      for (j = 0; j < pixels * 3 && read > CALIBRATION_SKIP_LINES;
		   j++)
		{
		  white_data[j] += buffer[j];
		}
	    }
	  if (is_black_line (buffer, pixels, MODE_COLOR) && black < 256)
	    {
	      black++;
	      for (j = 0; j < pixels * 3; j++)
		{
		  black_data[j] += buffer[j];
		}
	    }
	}
    }
  while (test_document (dev->fd) != SANE_STATUS_NO_DOCS);
  DBG (DBG_trace, "sheetfed_calibration: white lines=%d, black lines=%d\n",
       white, black);

  /* average pixels and store in per dpi calibration data */
  for (j = 0; j < pixels * 3; j++)
    {
      dev->calibration_data[i]->white_data[j] = white_data[j] / white;
      dev->calibration_data[i]->black_data[j] = black_data[j] / black;
    }

  /* we average red, green and blue offset on the full sensor */
  red = 0;
  green = 0;
  blue = 0;
  for (j = 0; j < pixels * 3; j += 3)
    {
      red += dev->calibration_data[i]->black_data[j];
      green += dev->calibration_data[i]->black_data[j + 1];
      blue += dev->calibration_data[i]->black_data[j + 2];
    }
  for (j = 0; j < pixels * 3; j += 3)
    {
      dev->calibration_data[i]->black_data[j] = red / pixels;
      dev->calibration_data[i]->black_data[j + 1] = green / pixels;
      dev->calibration_data[i]->black_data[j + 2] = blue / pixels;
    }

  /* trace calibration data for debug */
  if (DBG_LEVEL > DBG_data)
    {
      sprintf (title, "calibration-white-%d.pnm",
	       dev->calibration_data[i]->dpi);
      write_rgb_data (title, dev->calibration_data[i]->white_data, pixels, 1);
      sprintf (title, "calibration-black-%d.pnm",
	       dev->calibration_data[i]->dpi);
      write_rgb_data (title, dev->calibration_data[i]->black_data, pixels, 1);
    }

  /* loop on all remaining resolution and compute calibration data from it */
  for (i = 1; i < MAX_RESOLUTIONS && dev->model->xdpi_values[i] > 0; i++)
    {
      dev->calibration_data[i] =
	(P5_Calibration_Data *) malloc (sizeof (P5_Calibration_Data));
      if (dev->calibration_data[i] == NULL)
	{
	  cleanup_calibration (dev);
	  DBG (DBG_error,
	       "sheetfed_calibration: failed to allocate memory for calibration\n");
	  return SANE_STATUS_INVAL;
	}
      dev->calibration_data[i]->dpi = dev->model->xdpi_values[i];

      coeff = ((float) dev->model->xdpi_values[i]) / (float) dpi;

      /* generate data by decimation */
      for (j = 0; j < pixels / coeff; j++)
	{
	  k = j * coeff;
	  dev->calibration_data[i]->white_data[j] =
	    dev->calibration_data[0]->white_data[k];
	  dev->calibration_data[i]->white_data[j + 1] =
	    dev->calibration_data[0]->white_data[k + 1];
	  dev->calibration_data[i]->white_data[j + 2] =
	    dev->calibration_data[0]->white_data[k + 2];
	  dev->calibration_data[i]->black_data[j] =
	    dev->calibration_data[0]->black_data[k];
	  dev->calibration_data[i]->black_data[j + 1] =
	    dev->calibration_data[0]->black_data[k + 1];
	  dev->calibration_data[i]->black_data[j + 2] =
	    dev->calibration_data[0]->black_data[k + 2];
	}
    }

  fclose (dbg);
  dev->calibrated = SANE_TRUE;

  /* eject calibration target */
  eject (dev->fd);

  DBG (DBG_proc, "sheetfed_calibration: end\n");
  return SANE_STATUS_GOOD;
}

/* vim: set sw=2 cino=>2se-1sn-1s{s^-1st0(0u0 smarttab expandtab: */
