/****************************************************************************
 * drivers/eeprom/i2c_xx24xx.c
 *
 *   Copyright (C) 2018 Sebastien Lorquet. All rights reserved.
 *   Author: Sebastien Lorquet <sebastien@lorquet.fr>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/* This is a driver for I2C EEPROMs that use the same commands as the
 * xx24xx.
 *
 * The following devices should be supported:
 *
 * Manufacturer Device     Bytes PgSize AddrLen DevAddr
 * Microchip
 *              24xx00        16     1    1     1010000 Special case
 *              24xx01       128     8    1     1010000
 *              24xx02       256     8    1     1010000
 *              24xx04       512     16   1     101000P
 *              24xx08      1024     16   1     10100PP
 *              24xx16      2048     16   1     1010PPP
 *              24xx32      4096     32   2     1010AAA
 *              24xx64      8192     32   2     1010AAA
 *              24xx128    16384     64   2     1010AAA
 *              24xx256    32768     64   2     1010AAA
 *              24xx512    65536    128   2     1010AAA
 *              24xx1025  131072    128   2     1010PAA Special case: address bit is shifted.
 *              24xx1026  131072    128   2     1010AAP
 *
 * Atmel
 *              AT24C01      128     8    1     1010AAA
 *              AT24C02      256     8    1     1010AAA
 *              AT24C04      512    16    1     1010AAP P bits = word address
 *              AT24C08     1024    16    1     1010APP
 *              AT24C16     2048    16    1     1010PPP
 *              AT24C32     4096    32    2     1010AAA
 *              AT24C64     8192    32    2     1010AAA
 *              AT24C128   16384    64    2     10100AA
 *              AT24C256   32768    64    2     10100AA
 *              AT24C512   65536   128    2     10100AA
 *              AT24C1024 131072   256    2     10100AP
 *
 * ST Microelectronics
 *              M24C01       128    16    1     1010AAA
 *              M24C02       256    16    1     1010AAA
 *              M24C04       512    16    1     1010AAP
 *              M24C08      1024    16    1     1010APP
 *              M24C16      2048    16    1     1010PPP
 *              M24C32      4096    32    2     1010AAA ID pages supported as a separate device
 *              M24C64      8192    32    2     1010AAA
 *              M24128     16384    64    2     1010AAA
 *              M24256     32768    64    2     1010AAA
 *              M24512     65536   128    2     1010AAA
 *              M24M01    131072   256    2     1010AAP
 *              M24M02    262144   256    2     1010APP
 */

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <stdbool.h>
#include <sys/types.h>
#include <debug.h>
#include <errno.h>
#include <nuttx/fs/fs.h>

#include <nuttx/kmalloc.h>
#include <nuttx/i2c/i2c_master.h>
#include <nuttx/eeprom/i2c_xx24xx.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#ifndef CONFIG_EE24XX_FREQUENCY
#  define CONFIG_EE24XX_FREQUENCY 100000
#endif

/****************************************************************************
 * Types
 ****************************************************************************/

/* Device geometry description, compact form (2 bytes per entry) */

struct ee24xx_geom_s
{
  uint8_t bytes    : 4; /* Power of two of 128 bytes (0:128 ... 11:262144) */
  uint8_t pagesize : 4; /* Power of two of   8 bytes (0:8 1:16 2:32 3:64 etc) */
  uint8_t addrlen  : 4; /* Nr of bytes in command address field */
  uint8_t abits    : 3; /* Nr of Address MSBits in the i2c device address LSBs*/
  uint8_t special  : 1; /* Special device: uchip 24xx00 (total 16 bytes)
                         * or 24xx1025 (shifted P bits) */
};

/* Private data attached to the inode */

struct ee24xx_dev_s
{
  /* Bus management */

  struct i2c_master_s *i2c;      /* I2C device where the EEPROM is attached */
  uint32_t             freq;     /* I2C bus speed */
  uint8_t              addr;     /* 7-bit unshifted I2C device address */

  /* Driver management */

  sem_t                sem;      /* file write access serialization */
  uint8_t              refs;     /* Nr of times the device has been opened */
  bool                 readonly; /* Flags */

  /* Expanded from geometry */

  uint32_t             size;       /* total bytes in device */
  uint16_t             pgsize;     /* write block size, in bytes */
  uint16_t             addrlen;    /* number of bytes in data addresses */
  uint16_t             haddrbits;  /* Number of bits in high address part */
  uint16_t             haddrshift; /* bit-shift of high address part */
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static int     ee24xx_open(FAR struct file *filep);
static int     ee24xx_close(FAR struct file *filep);
static off_t   ee24xx_seek(FAR struct file *filep, off_t offset, int whence);
static ssize_t ee24xx_read(FAR struct file *filep, FAR char *buffer,
                           size_t buflen);
static ssize_t ee24xx_write(FAR struct file *filep, FAR const char *buffer,
                            size_t buflen);
static int     ee24xx_ioctl(FAR struct file *filep, int cmd,
                            unsigned long arg);

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* Supported device geometries.
 * One geometry can fit more than one device.
 * The user will use an enum'ed index from include/eeprom/i2c_xx24xx.h
 */

static const struct ee24xx_geom_s g_ee24xx_devices[] =
{
  /* Microchip devices */

/* by pg al ab sp      device     bytes page  alen */
  { 0, 1, 1, 0, 1}, /* 24xx00        16    1     1 Ridiculously small device */
  { 0, 0, 1, 0, 0}, /* 24xx01       128    8     1 */
  { 1, 0, 1, 0, 0}, /* 24xx02       256    8     1 */
  { 2, 1, 1, 1, 0}, /* 24xx04       512   16     1 */
  { 3, 1, 1, 2, 0}, /* 24xx08      1024   16     1 */
  { 4, 1, 1, 3, 0}, /* 24xx16      2048   16     1 */
  { 5, 2, 2, 0, 0}, /* 24xx32      4096   32     2 */
  { 6, 2, 2, 0, 0}, /* 24xx64      8192   32     2 */
  { 7, 3, 2, 0, 0}, /* 24xx128    16384   64     2 */
  { 8, 3, 2, 0, 0}, /* 24xx256    32768   64     2 */
  { 9, 4, 2, 0, 0}, /* 24xx512    65536  128     2 */
  {10, 4, 2, 1, 1}, /* 24xx1025  131072  128     2 Shifted address, todo */
  {10, 4, 2, 1, 0}, /* 24xx1026  131072  128     2 */
  {11, 5, 2, 2, 0}, /* AT24CM02  262144  256     2 */

  /* STM devices */

  { 0, 1, 1, 0, 0}, /* M24C01       128   16     1 */
  { 1, 1, 1, 0, 0}, /* M24C02       256   16     1 */
  {11, 5, 2, 2, 0}, /* M24M02    262144  256     2 */
};

/* Driver operations */

static const struct file_operations ee24xx_fops =
{
  ee24xx_open,  /* open */
  ee24xx_close, /* close */
  ee24xx_read,  /* read */
  ee24xx_write, /* write */
  ee24xx_seek,  /* seek */
  ee24xx_ioctl  /* ioctl */
#ifndef CONFIG_DISABLE_POLL
  , 0           /* poll */
#endif
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ee24xx_waitwritecomplete
 *
 * Use ACK polling to detect the completion of the write operation.
 * Returns TRUE if write is complete (device replies to ACK).
 * Note: The device always replies an ACK for the control byte, the polling
 * shall be done using the ACK for the memory address byte. Read or write does
 * not matter.
 * Note: We should sleep a bit between retries, the write time is around 5 ms,
 * but the bus is slow, so, a few retries at most will happen.
 *
 ****************************************************************************/

static int ee24xx_waitwritecomplete(FAR struct ee24xx_dev_s *eedev,
                                    uint32_t memaddr)
{
  struct i2c_msg_s msgs[1];
  int ret;
  int retries = 100;
  uint8_t adr;
  uint32_t addr_hi = (memaddr >> (eedev->addrlen << 3));

  msgs[0].frequency = eedev->freq;
  msgs[0].addr      = eedev->addr | (addr_hi & ((1 << eedev->haddrbits) - 1));
  msgs[0].flags     = I2C_M_READ;
  msgs[0].buffer    = &adr;
  msgs[0].length    = 1;

  do
    {
     ret = I2C_TRANSFER(eedev->i2c, msgs, 1);
     retries--;
    }
  while(ret != 0 && retries > 0);

  return ret;
}

/****************************************************************************
 * Name: ee24xx_writepage
 *
 * Description: Write data to the EEPROM, NOT crossing page boundaries.
 * To avoid allocating a buffer to prepend the address, we are using 2 I2C
 * messages while avoiding beginning the second one with a restart condition.
 *
 ****************************************************************************/

static int ee24xx_writepage(FAR struct ee24xx_dev_s *eedev, uint32_t memaddr,
                            FAR const char *buffer, size_t len)
{

  struct i2c_msg_s msgs[2];
  uint8_t maddr[2];
  uint32_t addr_hi = (memaddr >> (eedev->addrlen << 3));

  /* Write data address */

  maddr[0] = memaddr >> 8;
  maddr[1] = memaddr &  0xFF;

  msgs[0].frequency = eedev->freq;
  msgs[0].addr      = eedev->addr | (addr_hi & ((1 << eedev->haddrbits) - 1));
  msgs[0].flags     = 0;
  msgs[0].buffer    = eedev->addrlen==2 ? &maddr[0] : &maddr[1];
  msgs[0].length    = eedev->addrlen;

  /* Write data without a restart nor a control byte */

  msgs[1].frequency = msgs[0].frequency;
  msgs[1].addr      = msgs[0].addr;
  msgs[1].flags     = I2C_M_NOSTART;
  msgs[1].buffer    = (uint8_t*)buffer;
  msgs[1].length    = len;

  return I2C_TRANSFER(eedev->i2c, msgs, 2);
}

/****************************************************************************
 * Name: ee24xx_semtake
 *
 * Acquire a resource to access the device.
 * The purpose of the semaphore is to block tasks that try to access the
 * EEPROM while another task is actively using it.
 *
 ****************************************************************************/

static void ee24xx_semtake(FAR struct ee24xx_dev_s *eedev)
{
  /* Take the semaphore (perhaps waiting) */

  while (sem_wait(&eedev->sem) != 0)
    {
      /* The only case that an error should occur here is if
       * the wait was awakened by a signal.
       */

      DEBUGASSERT(errno == EINTR || errno == ECANCELED);
    }
}

/****************************************************************************
 * Name: ee24xx_semgive
 *
 * Release a resource to access the device.
 *
 ****************************************************************************/

static inline void ee24xx_semgive(FAR struct ee24xx_dev_s *eedev)
{
  sem_post(&eedev->sem);
}

/****************************************************************************
 * Driver Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ee24xx_open
 *
 * Description: Open the block device
 *
 ****************************************************************************/

static int ee24xx_open(FAR struct file *filep)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct ee24xx_dev_s *eedev;
  int ret = OK;

  DEBUGASSERT(inode && inode->i_private);
  eedev = (FAR struct ee24xx_dev_s *)inode->i_private;
  ee24xx_semtake(eedev);

  /* Increment the reference count */

  if ((eedev->refs + 1) == 0)
    {
      ret = -EMFILE;
    }
  else
    {
      eedev->refs += 1;
    }

  ee24xx_semgive(eedev);
  return ret;
}

/****************************************************************************
 * Name: ee24xx_close
 *
 * Description: Close the block device
 *
 ****************************************************************************/

static int ee24xx_close(FAR struct file *filep)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct ee24xx_dev_s *eedev;
  int ret = OK;

  DEBUGASSERT(inode && inode->i_private);
  eedev = (FAR struct ee24xx_dev_s *)inode->i_private;
  ee24xx_semtake(eedev);

  /* Decrement the reference count. I want the entire close operation
   * to be atomic wrt other driver operations.
   */

  if (eedev->refs == 0)
    {
      ret = -EIO;
    }
  else
    {
      eedev->refs -= 1;
    }

  ee24xx_semgive(eedev);
  return ret;
}

/****************************************************************************
 * Name: ee24xx_seek
 *
 * Remark: Copied from bchlib
 *
 ****************************************************************************/

static off_t ee24xx_seek(FAR struct file *filep, off_t offset, int whence)
{
  FAR struct ee24xx_dev_s *eedev;
  off_t                   newpos;
  int                     ret;
  FAR struct inode        *inode = filep->f_inode;

  DEBUGASSERT(inode && inode->i_private);
  eedev = (FAR struct ee24xx_dev_s *)inode->i_private;
  ee24xx_semtake(eedev);

  /* Determine the new, requested file position */

  switch (whence)
    {
    case SEEK_CUR:
      newpos = filep->f_pos + offset;
      break;

    case SEEK_SET:
      newpos = offset;
      break;

    case SEEK_END:
      newpos = eedev->size + offset;
      break;

    default:
      /* Return EINVAL if the whence argument is invalid */

      ee24xx_semgive(eedev);
      return -EINVAL;
    }

  /* Opengroup.org:
   *
   *  "The lseek() function shall allow the file offset to be set beyond the end
   *   of the existing data in the file. If data is later written at this point,
   *   subsequent reads of data in the gap shall return bytes with the value 0
   *   until data is actually written into the gap."
   *
   * We can conform to the first part, but not the second.  But return EINVAL if
   *
   *  "...the resulting file offset would be negative for a regular file, block
   *   special file, or directory."
   */

  if (newpos >= 0)
    {
      filep->f_pos = newpos;
      ret = newpos;
      finfo("SEEK newpos %d\n",newpos);
    }
  else
    {
      ret = -EINVAL;
    }

  ee24xx_semgive(eedev);
  return ret;
}

/****************************************************************************
 * Name: ee24xx_read
 ****************************************************************************/

static ssize_t ee24xx_read(FAR struct file *filep, FAR char *buffer,
                           size_t len)
{
  FAR struct ee24xx_dev_s *eedev;
  FAR struct inode        *inode = filep->f_inode;
  struct i2c_msg_s         msgs[2];
  uint8_t                  addr[2];
  uint32_t                 addr_hi;
  int                      ret;

  DEBUGASSERT(inode && inode->i_private);
  eedev = (FAR struct ee24xx_dev_s *)inode->i_private;

  ee24xx_semtake(eedev);

  /* trim len if read would go beyond end of device */

  if ((filep->f_pos + len) > eedev->size)
    {
      len = eedev->size - filep->f_pos;
    }

  if (len == 0)
    {
      /* We are at end of file */

      ret = 0;
      goto done;
    }

  /* Write data address */

  finfo("READ %d bytes at pos %d\n", len, filep->f_pos);

  addr_hi           = (filep->f_pos >> (eedev->addrlen << 3));

  addr[0]           = (filep->f_pos) >> 8;
  addr[1]           = (filep->f_pos) &  0xFF;

  msgs[0].frequency = eedev->freq;
  msgs[0].addr      = eedev->addr | (addr_hi & ((1 << eedev->haddrbits) - 1));
  msgs[0].flags     = 0;
  msgs[0].buffer    = eedev->addrlen==2 ? &addr[0] : &addr[1];
  msgs[0].length    = eedev->addrlen;

  /* Read data */

  msgs[1].frequency = msgs[0].frequency;
  msgs[1].addr      = msgs[0].addr;
  msgs[1].flags     = I2C_M_READ;
  msgs[1].buffer    = (uint8_t*)buffer;
  msgs[1].length    = len;

  ret = I2C_TRANSFER(eedev->i2c, msgs, 2);
  if (ret < 0)
    {
      goto done;
    }

  ret = len;

  /* Update the file position */

  filep->f_pos += len;

done:
  ee24xx_semgive(eedev);
  return ret;
}

/****************************************************************************
 * Name: ee24xx_write
 ****************************************************************************/

static ssize_t ee24xx_write(FAR struct file *filep, FAR const char *buffer,
                            size_t len)
{
  FAR struct ee24xx_dev_s *eedev;
  size_t                   cnt;
  int                      pageoff;
  FAR struct inode        *inode = filep->f_inode;
  int                      ret   = -EACCES;
  int                      savelen;

  DEBUGASSERT(inode && inode->i_private);
  eedev = (FAR struct ee24xx_dev_s *)inode->i_private;

  if (eedev->readonly)
    {
      return ret;
    }

  /* Forbid writes past the end of the device */

  if (filep->f_pos >= eedev->size)
    {
      return -EFBIG;
    }

  finfo("Entering with len=%d\n", len);

  /* Clamp len to avoid crossing the end of the memory */

  if ((len + filep->f_pos) > eedev->size)
    {
      len = eedev->size - filep->f_pos;
      finfo("Len clamped to %d\n", len);
    }

  savelen = len; /* save number of bytes written */

  ee24xx_semtake(eedev);

  /* Writes can't happen in a row like the read does.
   * The EEPROM is made of pages, and write sequences
   * cannot cross page boundaries. So every time the last
   * byte of a page is programmed, a separate I2C transaction
   * required to continue writing.
   */

  /* First, write some page-unaligned data */

  pageoff = filep->f_pos & (eedev->pgsize - 1);
  cnt     = eedev->pgsize - pageoff;
  if (cnt > len)
    {
      cnt = len;
    }

  if (pageoff > 0)
    {
      finfo("First %d unaligned bytes at %d (pageoff %d)\n", cnt, filep->f_pos,
            pageoff);

      ret = ee24xx_writepage(eedev, filep->f_pos, buffer, cnt);
      if (ret < 0)
        {
          ferr("write failed, ret = %d\n", ret);
          goto done;
        }

      ret = ee24xx_waitwritecomplete(eedev, filep->f_pos);
      if (ret < 0)
        {
          ferr("writecomplete failed, ret = %d\n", ret);
          goto done;
        }

      len          -= cnt;
      buffer       += cnt;
      filep->f_pos += cnt;
    }

  /* Then, write remaining bytes at page-aligned addresses */

  while (len > 0)
    {
      cnt = len;
      if (cnt > eedev->pgsize)
        {
          cnt = eedev->pgsize;
        }

      finfo("Aligned page write for %d bytes at %d\n", cnt, filep->f_pos);

      ret = ee24xx_writepage(eedev, filep->f_pos, buffer, cnt);
      if (ret < 0)
        {
          ferr("write failed, ret = %d\n", ret);
          goto done;
        }

      ret = ee24xx_waitwritecomplete(eedev, filep->f_pos);
      if (ret < 0)
        {
          ferr("writecomplete failed, ret = %d\n", ret);
          goto done;
        }

      len          -= cnt;
      buffer       += cnt;
      filep->f_pos += cnt;
    }

  ret = savelen;

done:
  ee24xx_semgive(eedev);
  return ret;
}

/****************************************************************************
 * Name: ee24xx_ioctl
 *
 * Description: TODO: Erase a sector/page/device or read device ID / MAC.
 * This is completely optional.
 *
 ****************************************************************************/

static int ee24xx_ioctl(FAR struct file *filep, int cmd, unsigned long arg)
{
  FAR struct ee24xx_dev_s *eedev;
  FAR struct inode        *inode = filep->f_inode;
  int                      ret   = 0;

  DEBUGASSERT(inode && inode->i_private);
  eedev = (FAR struct ee24xx_dev_s *)inode->i_private;

  switch (cmd)
    {
      default:
        (void)eedev;
        ret = -EINVAL;
    }

  return ret;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ee24xx_initialize
 *
 * Description: Bind a EEPROM driver to an I2C bus. The user MUST provide
 * a description of the device geometry, since it is not possible to read
 * this information from the device (contrary to the SPI flash devices).
 *
 ****************************************************************************/

int ee24xx_initialize(FAR struct i2c_master_s *bus, uint8_t devaddr,
                      FAR char *devname, int devtype, int readonly)
{
  FAR struct ee24xx_dev_s *eedev;

  /* Check device type early */

  if ((devtype < 0) ||
      (devtype >= sizeof(g_ee24xx_devices) / sizeof(g_ee24xx_devices[0])))
    {
     return -EINVAL;
    }

  eedev = kmm_zalloc(sizeof(struct ee24xx_dev_s));

  if (!eedev)
    {
      return -ENOMEM;
    }

  sem_init(&eedev->sem, 0, 1);

  eedev->freq     = CONFIG_EE24XX_FREQUENCY;
  eedev->i2c      = bus;
  eedev->addr     = devaddr;
  eedev->readonly = !!readonly;

  /* Expand device geometry from compacted info */

  eedev->size       = 128 << g_ee24xx_devices[devtype].bytes;
  eedev->pgsize     =   8 << g_ee24xx_devices[devtype].pagesize;
  eedev->addrlen    =        g_ee24xx_devices[devtype].addrlen;
  eedev->haddrbits  =        g_ee24xx_devices[devtype].abits;
  eedev->haddrshift = 0;

  /* Apply special properties */

  if (g_ee24xx_devices[devtype].special)
    {
      if (devtype == EEPROM_24xx00)
        {
          /* Ultra small 16-byte EEPROM */

          eedev->size  = 16;

          /* The device only has BYTE write,
           * which is emulated with 1-byte pages
           */

          eedev->pgsize = 1;
        }
      else if (devtype == EEPROM_24xx1025)
        {
          /* Microchip alien part where the address MSB is << 2 bits */

          ferr("Device 24xx1025 is not supported for the moment, TODO.\n");

          eedev->haddrshift = 2;
          free(eedev);
          return -ENODEV;
        }
    }

  finfo("EEPROM device %s, %d bytes, %d per page, addrlen %d, %s\n",
        devname, eedev->size, eedev->pgsize, eedev->addrlen,
        eedev->readonly ? "readonly" : "");

  return register_driver(devname, &ee24xx_fops, 0666, eedev);
}