/****************************************************************************
 * drivers/mtd/dhara.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

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

#include <nuttx/config.h>

#include <errno.h>
#include <debug.h>
#include <stdio.h>

#include <nuttx/nuttx.h>
#include <nuttx/kmalloc.h>
#include <nuttx/mtd/mtd.h>

#include <dhara/map.h>
#include <dhara/nand.h>

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

/****************************************************************************
 * Private Types
 ****************************************************************************/

struct dhara_pagecache_s
{
  dq_entry_t   node;
  dhara_page_t page;
  FAR uint8_t *buffer;
};

typedef struct dhara_pagecache_s dhara_pagecache_t;

struct dhara_dev_s
{
  struct dhara_nand     nand;
  struct dhara_map      map;
  mutex_t               lock;

  FAR struct mtd_dev_s *mtd;      /* Contained MTD interface */
  struct mtd_geometry_s geo;      /* Device geometry */
  uint16_t              blkper;   /* R/W blocks per erase block */
  uint16_t              refs;     /* Number of references */
  bool                  unlinked; /* The driver has been unlinked */

  /* Two pagesize buffer first is for working temp buffer
   * second is for journel use
   */

  FAR uint8_t *pagebuf;

  /* Read cache for accelerate read dhara meta data */

  struct dq_queue_s readcache;
  dhara_pagecache_t readpage[CONFIG_DHARA_READ_NCACHES];
};

typedef struct dhara_dev_s dhara_dev_t;

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

static int     dhara_open(FAR struct inode *inode);
static int     dhara_close(FAR struct inode *inode);
static ssize_t dhara_read(FAR struct inode *inode,
                          FAR unsigned char *buffer,
                          blkcnt_t start_sector,
                          unsigned int nsectors);
static ssize_t dhara_write(FAR struct inode *inode,
                           FAR const unsigned char *buffer,
                           blkcnt_t start_sector,
                           unsigned int nsectors);
static int     dhara_geometry(FAR struct inode *inode,
                              FAR struct geometry *geometry);
static int     dhara_ioctl(FAR struct inode *inode,
                           int cmd,
                           unsigned long arg);
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
static int     dhara_unlink(FAR struct inode *inode);
#endif

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

static const struct block_operations g_dhara_bops =
{
  dhara_open,     /* open     */
  dhara_close,    /* close    */
  dhara_read,     /* read     */
  dhara_write,    /* write    */
  dhara_geometry, /* geometry */
  dhara_ioctl     /* ioctl    */
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  , dhara_unlink  /* unlink   */
#endif
};

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

static int dhara_convert_result(dhara_error_t err)
{
  switch (err)
    {
      case DHARA_E_NONE:
        return 0;

      case DHARA_E_BAD_BLOCK:
      case DHARA_E_ECC:
      case DHARA_E_TOO_BAD:
      case DHARA_E_CORRUPT_MAP:
        return -EBADMSG;

      case DHARA_E_JOURNAL_FULL:
      case DHARA_E_MAP_FULL:
        return -ENOSPC;

      case DHARA_E_NOT_FOUND:
        return -ENOENT;

      default:
        return  -EFAULT;
    }
}

static bool dhara_check_ff(FAR const uint8_t *buf, size_t size)
{
  FAR const uint8_t *p = buf;
  size_t len;

  for (len = 0; len < 16; len++)
    {
      if (!size)
          return true;

      if (*p != 0xff)
          return false;

      p++;
      size--;
    }

  return memcmp(buf, p, size) == 0;
}

static int dhara_init_readcache(FAR dhara_dev_t *dev)
{
  FAR dq_queue_t *q = &dev->readcache;
  int i = 0;

  dq_init(q);

  do
    {
      dev->readpage[i].page = DHARA_PAGE_NONE;
      dev->readpage[i].buffer = kmm_malloc(dev->geo.blocksize);
      dq_addfirst(&dev->readpage[i].node, q);
    }
  while (dev->readpage[i++].buffer && i < CONFIG_DHARA_READ_NCACHES);

  return i == CONFIG_DHARA_READ_NCACHES ? 0 : -ENOMEM;
}

static void dhara_deinit_readcache(FAR dhara_dev_t *dev)
{
  int i;

  for (i = 0; i < CONFIG_DHARA_READ_NCACHES; i++)
    {
      if (dev->readpage[i].buffer)
        {
          kmm_free(dev->readpage[i].buffer);
        }
    }
}

static uint8_t *dhara_find_readcache(FAR dhara_dev_t *dev,
                                     dhara_page_t page)
{
  FAR dq_queue_t *q = &dev->readcache;
  FAR dhara_pagecache_t *cache;
  FAR dq_entry_t *c;

  for (c = dq_peek(q); c; c = dq_next(c))
    {
      cache = (FAR dhara_pagecache_t *)c;
      if (cache->page == page)
        {
          dq_rem(c, q);
          dq_addfirst(c, q);
          return cache->buffer;
        }
    }

  return NULL;
}

static dhara_pagecache_t *dhara_grab_readcache(FAR dhara_dev_t *dev)
{
  FAR dq_queue_t *q = &dev->readcache;
  FAR dhara_pagecache_t *cache;
  FAR dq_entry_t *c;

  c = dq_tail(q);
  dq_rem(c, q);
  cache = (FAR dhara_pagecache_t *)c;
  cache->page = DHARA_PAGE_NONE;
  return cache;
}

static void dhara_insert_readcache(FAR dhara_dev_t *dev,
                                   dhara_pagecache_t *cache)
{
  FAR dq_queue_t *q = &dev->readcache;

  if (cache->page != DHARA_PAGE_NONE)
      dq_addfirst((dq_entry_t *)cache, q);
  else
      dq_addlast((dq_entry_t *)cache, q);
}

static void dhara_discard_readcache(FAR dhara_dev_t *dev,
                                    dhara_page_t page)
{
  FAR dhara_pagecache_t *cache;
  FAR dq_queue_t *q = &dev->readcache;
  FAR dq_entry_t *c;

  for (c = dq_peek(q); c; c = dq_next(c))
    {
      cache = (FAR dhara_pagecache_t *)c;
      if (cache->page == page)
        {
          cache->page = DHARA_PAGE_NONE;
          dq_rem(c, q);
          dq_addlast(c, q);
          break;
        }
    }
}

static void dhara_update_readcache(FAR dhara_dev_t *dev,
                                    dhara_page_t page,
                                    FAR const uint8_t *data)
{
  FAR dhara_pagecache_t *cache;
  FAR dq_queue_t *q = &dev->readcache;
  FAR dq_entry_t *c;

  for (c = dq_peek(q); c; c = dq_next(c))
    {
      cache = (FAR dhara_pagecache_t *)c;
      if (cache->page == page)
        {
          cache->page = page;
          memcpy(cache->buffer, data, dev->geo.blocksize);
          dq_rem(c, q);
          dq_addfirst(c, q);
          break;
        }
    }
}

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

static int dhara_open(FAR struct inode *inode)
{
  FAR dhara_dev_t *dev;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;
  nxmutex_lock(&dev->lock);
  dev->refs++;
  nxmutex_unlock(&dev->lock);

  return 0;
}

/****************************************************************************
 * Name: dhara_close
 *
 * Description: close the block device
 *
 ****************************************************************************/

static int dhara_close(FAR struct inode *inode)
{
  FAR dhara_dev_t *dev;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;
  nxmutex_lock(&dev->lock);
  dev->refs--;
  nxmutex_unlock(&dev->lock);

  if (dev->refs == 0 && dev->unlinked)
    {
      nxmutex_destroy(&dev->lock);
      dhara_deinit_readcache(dev);
      kmm_free(dev->pagebuf);
      kmm_free(dev);
    }

  return 0;
}

/****************************************************************************
 * Name: dhara_read
 *
 * Description:  Read the specified number of sectors
 *
 ****************************************************************************/

static ssize_t dhara_read(FAR struct inode *inode,
                          FAR unsigned char *buffer,
                          blkcnt_t start_sector,
                          unsigned int nsectors)
{
  FAR dhara_dev_t *dev;
  size_t nread = 0;
  int ret = 0;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;

  nxmutex_lock(&dev->lock);
  while (nsectors-- > 0)
    {
      dhara_error_t err;
      ret = dhara_map_read(&dev->map,
                           start_sector,
                           buffer,
                           &err);
      if (ret < 0)
        {
          ret = dhara_convert_result(err);
          ferr("Read startblock %lld failed nread %zd err: %s\n",
               (long long)start_sector, nread, dhara_strerror(err));
          break;
        }

      nread++;
      start_sector++;
      buffer += dev->geo.blocksize;
    }

  nxmutex_unlock(&dev->lock);
  return nread ? nread : ret;
}

/****************************************************************************
 * Name: dhara_write
 *
 * Description: Write (or buffer) the specified number of sectors
 *
 ****************************************************************************/

static ssize_t dhara_write(FAR struct inode *inode,
                           FAR const unsigned char *buffer,
                           blkcnt_t start_sector,
                           unsigned int nsectors)
{
  FAR dhara_dev_t *dev;
  size_t nwrite = 0;
  int ret = 0;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;

  nxmutex_lock(&dev->lock);
  while (nsectors-- > 0)
    {
      dhara_error_t err;
      ret = dhara_map_write(&dev->map,
                            start_sector,
                            buffer,
                            &err);
      if (ret < 0)
        {
          ret = dhara_convert_result(err);
          ferr("Write starting at block %lld failed nwrite %zu err %s\n",
               (long long)start_sector, nwrite, dhara_strerror(err));
          break;
        }

      nwrite++;
      start_sector++;
      buffer += dev->geo.blocksize;
    }

  nxmutex_unlock(&dev->lock);
  return nwrite ? nwrite : ret;
}

/****************************************************************************
 * Name: dhara_geometry
 *
 * Description: Return device geometry
 *
 ****************************************************************************/

static int dhara_geometry(FAR struct inode *inode,
                          FAR struct geometry *geometry)
{
  FAR dhara_dev_t *dev;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;

  if (geometry)
    {
      geometry->geo_available    = true;
      geometry->geo_mediachanged = false;
      geometry->geo_writeenabled = true;
      geometry->geo_nsectors     = dev->geo.neraseblocks * dev->blkper;
      geometry->geo_sectorsize   = dev->geo.blocksize;

      strcpy(geometry->geo_model, dev->geo.model);
      nxmutex_unlock(&dev->lock);
      return 0;
    }

  return -EINVAL;
}

/****************************************************************************
 * Name: dhara_ioctl
 *
 * Description: Return device geometry
 *
 ****************************************************************************/

static int dhara_ioctl(FAR struct inode *inode,
                       int cmd,
                       unsigned long arg)
{
  FAR dhara_dev_t *dev;
  int ret;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;

  /* No other block driver ioctl commands are not recognized by this
   * driver.  Other possible MTD driver ioctl commands are passed through
   * to the MTD driver (unchanged).
   */

  ret = MTD_IOCTL(dev->mtd, cmd, arg);
  if (ret < 0 && ret != -ENOTTY)
    {
      ferr("MTD ioctl(%04x) failed: %d\n", cmd, ret);
    }

  return ret;
}

/****************************************************************************
 * Name: dhara_unlink
 *
 * Description: Unlink the device
 *
 ****************************************************************************/

#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
static int dhara_unlink(FAR struct inode *inode)
{
  FAR dhara_dev_t *dev;

  DEBUGASSERT(inode->i_private);
  dev = inode->i_private;
  nxmutex_lock(&dev->lock);
  dev->unlinked = true;
  nxmutex_unlock(&dev->lock);

  if (dev->refs == 0)
    {
      nxmutex_destroy(&dev->lock);
      dhara_deinit_readcache(dev);
      kmm_free(dev->pagebuf);
      kmm_free(dev);
    }

  return 0;
}
#endif

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

/* dhara nand interface implement */

int dhara_nand_is_bad(FAR const struct dhara_nand *n,
                      dhara_block_t bno)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  return MTD_ISBAD(dev->mtd, bno);
}

void dhara_nand_mark_bad(FAR const struct dhara_nand *n,
                         dhara_block_t bno)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  MTD_MARKBAD(dev->mtd, bno);
}

int dhara_nand_erase(FAR const struct dhara_nand *n,
                     dhara_block_t bno,
                     FAR dhara_error_t *err)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  dhara_page_t pno = bno << n->log2_ppb;
  int ret;
  int i;

  ret = MTD_ERASE(dev->mtd, bno, 1);
  if (ret < 0)
    {
      dhara_set_error(err, DHARA_E_BAD_BLOCK);
      return ret;
    }

  for (i = 0; i < dev->blkper; i++)
    {
      dhara_discard_readcache(dev, pno + i);
    }

  return 0;
}

int dhara_nand_prog(FAR const struct dhara_nand *n,
                    dhara_page_t p,
                    FAR const uint8_t *data,
                    FAR dhara_error_t *err)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  int ret;

  ret = MTD_BWRITE(dev->mtd, p, 1, data);
  if (ret < 0)
    {
      dhara_set_error(err, DHARA_E_BAD_BLOCK);
      return ret;
    }

  dhara_update_readcache(dev, p, data);
  return 0;
}

int dhara_nand_is_free(FAR const struct dhara_nand *n,
                       dhara_page_t p)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  size_t page_size = 1 << n->log2_page_size;
  FAR uint8_t *buf = dev->pagebuf;
  dhara_error_t err;

  if (dhara_nand_read(n, p, 0, page_size, buf, &err) < 0)
    {
      ferr("Fail to read page for free check err %s\n",
            dhara_strerror(err));
      return 0;
    }

  return dhara_check_ff(buf, page_size);
}

int dhara_nand_read(FAR const struct dhara_nand *n,
                    dhara_page_t p,
                    size_t offset,
                    size_t length,
                    FAR uint8_t *data,
                    dhara_error_t *err)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  FAR dhara_pagecache_t *cache;
  FAR uint8_t *buf;
  int ret;

  buf = dhara_find_readcache(dev, p);
  if (buf)
    {
      memcpy(data, buf + offset, length);
      return 0;
    }

  cache = dhara_grab_readcache(dev);
  ret = MTD_BREAD(dev->mtd, p, 1, cache->buffer);
  if (ret == -EUCLEAN)
    {
      ret = 0; /* Ignore the correctable ECC error */
    }

  if (ret < 0)
    {
      dhara_insert_readcache(dev, cache);
      dhara_set_error(err, DHARA_E_ECC);
      return ret;
    }

  memcpy(data, cache->buffer + offset, length);
  cache->page = p;
  dhara_insert_readcache(dev, cache);
  return ret;
}

int dhara_nand_copy(FAR const struct dhara_nand *n,
                    dhara_page_t src,
                    dhara_page_t dst,
                    FAR dhara_error_t *err)
{
  FAR dhara_dev_t *dev = (FAR dhara_dev_t *)n;
  size_t page_size = 1 << n->log2_page_size;
  FAR uint8_t *buf = dev->pagebuf;
  int ret;

  ret = dhara_nand_read(n, src, 0, page_size, buf, err);
  if (ret < 0)
    {
      ferr("src_page %d, ret %d failed: %s\n",
            src, ret, dhara_strerror(*err));
      return ret;
    }

  ret = dhara_nand_prog(n, dst, buf, err);
  if (ret < 0)
    {
      ferr("dst_page %d, ret %d failed: %s\n",
            dst, ret, dhara_strerror(*err));
      return ret;
    }

  return 0;
}

/****************************************************************************
 * Name: dhara_initialize_by_path
 *
 * Description:
 *   Initialize to provide a block driver wrapper around an MTD interface
 *
 * Input Parameters:
 *   path - The block device path.
 *   mtd  - The MTD device that supports the FLASH interface.
 *
 ****************************************************************************/

int dhara_initialize_by_path(FAR const char *path,
                             FAR struct mtd_dev_s *mtd)
{
  FAR dhara_dev_t *dev;
  int ret;

  /* Sanity check */

  if (path == NULL || mtd == NULL)
    {
      return -EINVAL;
    }

  /* Allocate a DHARA_MTDBLOCK device structure */

  dev = (FAR dhara_dev_t *)
                 kmm_zalloc(sizeof(dhara_dev_t));
  if (dev == NULL)
    {
      return -ENOMEM;
    }

  nxmutex_init(&dev->lock);

  /* Initialize the DHARA_MTDBLOCK device structure */

  dev->mtd = mtd;

  /* Get the device geometry. (casting to uintptr_t first
   * eliminates complaints on some architectures where the
   * sizeof long is different
   * from the size of a pointer).
   */

  ret = MTD_IOCTL(mtd,
                  MTDIOC_GEOMETRY,
                  (unsigned long)((uintptr_t)&dev->geo));
  if (ret < 0)
    {
      ferr("MTD ioctl(MTDIOC_GEOMETRY) failed: %d\n", ret);
      goto err;
    }

  /* Get the number of R/W blocks per erase block */

  dev->blkper = dev->geo.erasesize / dev->geo.blocksize;
  DEBUGASSERT(dev->blkper * dev->geo.blocksize == dev->geo.erasesize);

  /* Init dhara */

  dev->nand.log2_page_size = fls(dev->geo.blocksize) - 1;
  dev->nand.log2_ppb = fls(dev->blkper) - 1;
  dev->nand.num_blocks = dev->geo.neraseblocks;

  dev->pagebuf = kmm_zalloc(dev->geo.blocksize * 2);
  if (!dev->pagebuf)
    {
      ret = -ENOMEM;
      goto err;
    }

  ret = dhara_init_readcache(dev);
  if (ret != 0)
    {
      goto err;
    }

  dhara_map_init(&dev->map, &dev->nand,
                 dev->pagebuf + dev->geo.blocksize,
                 CONFIG_DHARA_GC_RATIO);

  dhara_map_resume(&dev->map, NULL);

  /* Inode private data is a reference to the
   * DHARA_MTDBLOCK device structure
   */

  ret = register_blockdriver(path, &g_dhara_bops, 0666, dev);
  if (ret < 0)
    {
      ferr("register_blockdriver failed: %d\n", ret);
      goto err;
    }

  return ret;

err:
  nxmutex_destroy(&dev->lock);
  dhara_deinit_readcache(dev);
  kmm_free(dev->pagebuf);
  kmm_free(dev);
  return ret;
}

/****************************************************************************
 * Name: dhara_initialize
 *
 * Description:
 *   Initialize to provide a block driver wrapper around an MTD interface
 *
 * Input Parameters:
 *   minor - The minor device number.  The MTD block device will be
 *           registered as as /dev/mtdblockN where N is the minor number.
 *   mtd   - The MTD device that supports the FLASH interface.
 *
 ****************************************************************************/

int dhara_initialize(int minor, FAR struct mtd_dev_s *mtd)
{
  char path[PATH_MAX];

#ifdef CONFIG_DEBUG_FEATURES
  /* Sanity check */

  if (minor < 0 || minor > 255)
    {
      return -EINVAL;
    }
#endif

  /* Do the real work by dhara_mtdblock_initialize_by_path */

  snprintf(path, sizeof(path), "/dev/mtdblock%d", minor);
  return dhara_initialize_by_path(path, mtd);
}