/****************************************************************************
 * drivers/mtd/rammtd.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 <sys/types.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <debug.h>

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

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

/* Configuration ************************************************************/

#ifndef CONFIG_RAMMTD_BLOCKSIZE
#  define CONFIG_RAMMTD_BLOCKSIZE 512
#endif

#ifndef CONFIG_RAMMTD_ERASESIZE
#  define CONFIG_RAMMTD_ERASESIZE 4096
#endif

#ifndef CONFIG_RAMMTD_ERASESTATE
#  define CONFIG_RAMMTD_ERASESTATE 0xff
#endif

#if CONFIG_RAMMTD_ERASESTATE != 0xff && CONFIG_RAMMTD_ERASESTATE != 0x00
#  error "Unsupported value for CONFIG_RAMMTD_ERASESTATE"
#endif

#if CONFIG_RAMMTD_BLOCKSIZE > CONFIG_RAMMTD_ERASESIZE
#  error "Must have CONFIG_RAMMTD_BLOCKSIZE <= CONFIG_RAMMTD_ERASESIZE"
#endif

#undef  RAMMTD_BLKPER
#define RAMMTD_BLKPER (CONFIG_RAMMTD_ERASESIZE/CONFIG_RAMMTD_BLOCKSIZE)

#if RAMMTD_BLKPER*CONFIG_RAMMTD_BLOCKSIZE != CONFIG_RAMMTD_ERASESIZE
#  error "CONFIG_RAMMTD_ERASESIZE must be an even multiple of CONFIG_RAMMTD_BLOCKSIZE"
#endif

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

/* This type represents the state of the MTD device.  The struct mtd_dev_s
 * must appear at the beginning of the definition so that you can freely
 * cast between pointers to struct mtd_dev_s and struct ram_dev_s.
 */

struct ram_dev_s
{
  struct mtd_dev_s mtd;      /* MTD device */
  FAR uint8_t     *start;    /* Start of RAM */
  size_t           nblocks;  /* Number of erase blocks */
};

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

/* The RAM MTD driver may be useful just as it is, but another good use of
 * the RAM MTD driver is as a FLASH simulation -- to support testing of FLASH
 * based logic without having FLASH.  CONFIG_RAMMTD_FLASHSIM will add some
 * extra logic to improve the level of FLASH simulation.
 */

#define ram_read(dest, src, len) memcpy(dest, src, len)
#ifdef CONFIG_RAMMTD_FLASHSIM
static void *ram_write(FAR void *dest, FAR const void *src, size_t len);
#else
#  define ram_write(dest, src, len) memcpy(dest, src, len)
#endif

/* MTD driver methods */

static int ram_erase(FAR struct mtd_dev_s *dev,
                     off_t startblock,
                     size_t nblocks);
static ssize_t ram_bread(FAR struct mtd_dev_s *dev,
                         off_t startblock,
                         size_t nblocks,
                         FAR uint8_t *buf);
static ssize_t ram_bwrite(FAR struct mtd_dev_s *dev,
                          off_t startblock,
                          size_t nblocks,
                          FAR const uint8_t *buf);
static ssize_t ram_byteread(FAR struct mtd_dev_s *dev,
                            off_t offset,
                            size_t nbytes, FAR uint8_t *buf);
#ifdef CONFIG_MTD_BYTE_WRITE
static ssize_t ram_bytewrite(FAR struct mtd_dev_s *dev,
                             off_t offset,
                             size_t nbytes,
                             FAR const uint8_t *buf);
#endif
static int ram_ioctl(FAR struct mtd_dev_s *dev,
                     int cmd,
                     unsigned long arg);

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

/****************************************************************************
 * Name: ram_write
 ****************************************************************************/

#ifdef CONFIG_RAMMTD_FLASHSIM
static void *ram_write(FAR void *dest, FAR const void *src, size_t len)
{
  FAR uint8_t       *pout = (FAR uint8_t *)dest;
  FAR const uint8_t *pin  = (FAR const uint8_t *)src;

  while (len-- > 0)
    {
      /* Get the source and destination values */

      uint8_t oldvalue = *pout;
      uint8_t srcvalue = *pin++;
      uint8_t newvalue;

      /* Get the new destination value, accounting for bits that cannot be
       * changes because they are not in the erased state.
       */

#if CONFIG_RAMMTD_ERASESTATE == 0xff
      newvalue = oldvalue & srcvalue; /* We can only clear bits */
#else /* CONFIG_RAMMTD_ERASESTATE == 0x00 */
      newvalue = oldvalue | srcvalue; /* We can only set bits */
#endif

      /* Report any attempt to change the value of bits that are not in the
       * erased state.
       */

#ifdef CONFIG_DEBUG_FEATURES
      if (newvalue != srcvalue)
        {
          _err("ERROR: Bad write: source=%02x dest=%02x result=%02x\n",
              srcvalue, oldvalue, newvalue);
        }
#endif

      /* Write the modified value to simulated FLASH */

      *pout++ = newvalue;
    }

  return dest;
}
#endif

/****************************************************************************
 * Name: ram_erase
 ****************************************************************************/

static int ram_erase(FAR struct mtd_dev_s *dev, off_t startblock,
                     size_t nblocks)
{
  FAR struct ram_dev_s *priv = (FAR struct ram_dev_s *)dev;
  off_t offset;
  size_t nbytes;

  DEBUGASSERT(dev);

  /* Don't let the erase exceed the size of the ram buffer */

  if (startblock >= priv->nblocks)
    {
      return 0;
    }

  if (startblock + nblocks > priv->nblocks)
    {
      nblocks = priv->nblocks - startblock;
    }

  /* Convert the erase block to a logical block and the number of blocks
   * in logical block numbers
   */

  startblock *= RAMMTD_BLKPER;
  nblocks    *= RAMMTD_BLKPER;

  /* Get the offset corresponding to the first block and the size
   * corresponding to the number of blocks.
   */

  offset = startblock * CONFIG_RAMMTD_BLOCKSIZE;
  nbytes = nblocks * CONFIG_RAMMTD_BLOCKSIZE;

  /* Then erase the data in RAM */

  memset(&priv->start[offset], CONFIG_RAMMTD_ERASESTATE, nbytes);
  return OK;
}

/****************************************************************************
 * Name: ram_bread
 ****************************************************************************/

static ssize_t ram_bread(FAR struct mtd_dev_s *dev,
                         off_t startblock,
                         size_t nblocks,
                         FAR uint8_t *buf)
{
  FAR struct ram_dev_s *priv = (FAR struct ram_dev_s *)dev;
  off_t offset;
  off_t maxblock;
  size_t nbytes;

  DEBUGASSERT(dev && buf);

  /* Don't let the read exceed the size of the ram buffer */

  maxblock = priv->nblocks * RAMMTD_BLKPER;
  if (startblock >= maxblock)
    {
      return 0;
    }

  if (startblock + nblocks > maxblock)
    {
      nblocks = maxblock - startblock;
    }

  /* Get the offset corresponding to the first block and the size
   * corresponding to the number of blocks.
   */

  offset = startblock * CONFIG_RAMMTD_BLOCKSIZE;
  nbytes = nblocks * CONFIG_RAMMTD_BLOCKSIZE;

  /* Then read the data frp, RAM */

  ram_read(buf, &priv->start[offset], nbytes);
  return nblocks;
}

/****************************************************************************
 * Name: ram_bwrite
 ****************************************************************************/

static ssize_t ram_bwrite(FAR struct mtd_dev_s *dev, off_t startblock,
                          size_t nblocks, FAR const uint8_t *buf)
{
  FAR struct ram_dev_s *priv = (FAR struct ram_dev_s *)dev;
  off_t offset;
  off_t maxblock;
  size_t nbytes;

  DEBUGASSERT(dev && buf);

  /* Don't let the write exceed the size of the ram buffer */

  maxblock = priv->nblocks * RAMMTD_BLKPER;
  if (startblock >= maxblock)
    {
      return 0;
    }

  if (startblock + nblocks > maxblock)
    {
      nblocks = maxblock - startblock;
    }

  /* Get the offset corresponding to the first block and the size
   * corresponding to the number of blocks.
   */

  offset = startblock * CONFIG_RAMMTD_BLOCKSIZE;
  nbytes = nblocks * CONFIG_RAMMTD_BLOCKSIZE;

  /* Then write the data to RAM */

  ram_write(&priv->start[offset], buf, nbytes);
  return nblocks;
}

/****************************************************************************
 * Name: ram_byteread
 ****************************************************************************/

static ssize_t ram_byteread(FAR struct mtd_dev_s *dev, off_t offset,
                            size_t nbytes, FAR uint8_t *buf)
{
  FAR struct ram_dev_s *priv = (FAR struct ram_dev_s *)dev;
  off_t maxoffset;

  DEBUGASSERT(dev && buf);

  /* Don't let the read exceed the size of the ram buffer */

  maxoffset = priv->nblocks * CONFIG_RAMMTD_ERASESIZE;
  if (offset >= maxoffset)
    {
      return 0;
    }

  if (offset + nbytes > maxoffset)
    {
      nbytes = maxoffset - offset;
    }

  ram_read(buf, &priv->start[offset], nbytes);
  return nbytes;
}

/****************************************************************************
 * Name: ram_bytewrite
 ****************************************************************************/

#ifdef CONFIG_MTD_BYTE_WRITE
static ssize_t ram_bytewrite(FAR struct mtd_dev_s *dev, off_t offset,
                             size_t nbytes, FAR const uint8_t *buf)
{
  FAR struct ram_dev_s *priv = (FAR struct ram_dev_s *)dev;
  off_t maxoffset;

  DEBUGASSERT(dev && buf);

  /* Don't let the write exceed the size of the ram buffer */

  maxoffset = priv->nblocks * CONFIG_RAMMTD_ERASESIZE;
  if (offset >= maxoffset)
    {
      return 0;
    }

  if (offset + nbytes > maxoffset)
    {
      nbytes = maxoffset - offset;
    }

  /* Then write the data to RAM */

  ram_write(&priv->start[offset], buf, nbytes);
  return nbytes;
}
#endif

/****************************************************************************
 * Name: ram_ioctl
 ****************************************************************************/

static int ram_ioctl(FAR struct mtd_dev_s *dev, int cmd, unsigned long arg)
{
  FAR struct ram_dev_s *priv = (FAR struct ram_dev_s *)dev;
  int ret = -EINVAL; /* Assume good command with bad parameters */

  switch (cmd)
    {
      case MTDIOC_GEOMETRY:
        {
          FAR struct mtd_geometry_s *geo =
                              (FAR struct mtd_geometry_s *)((uintptr_t)arg);
          if (geo)
            {
              memset(geo, 0, sizeof(*geo));

              /* Populate the geometry structure with information need to
               * know the capacity and how to access the device.
               */

              geo->blocksize    = CONFIG_RAMMTD_BLOCKSIZE;
              geo->erasesize    = CONFIG_RAMMTD_ERASESIZE;
              geo->neraseblocks = priv->nblocks;
              ret               = OK;
          }
        }
        break;

      case BIOC_XIPBASE:
        {
          FAR void **ppv = (FAR void**)((uintptr_t)arg);
          if (ppv)
            {
              /* Return (void*) base address of device memory */

              *ppv = (FAR void *)priv->start;
              ret  = OK;
            }
        }
        break;

      case BIOC_PARTINFO:
        {
          FAR struct partition_info_s *info =
            (FAR struct partition_info_s *)arg;
          if (info != NULL)
            {
              info->numsectors  = priv->nblocks *
                                  CONFIG_RAMMTD_ERASESIZE /
                                  CONFIG_RAMMTD_BLOCKSIZE;
              info->sectorsize  = CONFIG_RAMMTD_BLOCKSIZE;
              info->startsector = 0;
              info->parent[0]   = '\0';
              ret               = OK;
            }
        }
        break;

      case MTDIOC_BULKERASE:
        {
            size_t size = priv->nblocks * CONFIG_RAMMTD_ERASESIZE;

            /* Erase the entire device */

            memset(priv->start, CONFIG_RAMMTD_ERASESTATE, size);
            ret = OK;
        }
        break;

      case MTDIOC_ERASESTATE:
        {
          FAR uint8_t *result = (FAR uint8_t *)arg;
          *result = CONFIG_RAMMTD_ERASESTATE;

          ret = OK;
        }
        break;

      default:
        ret = -ENOTTY; /* Bad command */
        break;
    }

  return ret;
}

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

/****************************************************************************
 * Name: rammtd_initialize
 *
 * Description:
 *   Create and initialize a RAM MTD device instance.
 *
 * Input Parameters:
 *   start - Address of the beginning of the allocated RAM regions.
 *   size  - The size in bytes of the allocated RAM region.
 *
 ****************************************************************************/

FAR struct mtd_dev_s *rammtd_initialize(FAR uint8_t *start, size_t size)
{
  FAR struct ram_dev_s *priv;
  size_t nblocks;

  /* Create an instance of the RAM MTD device state structure */

  priv = kmm_zalloc(sizeof(struct ram_dev_s));
  if (!priv)
    {
      ferr("ERROR: Failed to allocate the RAM MTD state structure\n");
      return NULL;
    }

  /* Force the size to be an even number of the erase block size */

  nblocks = size / CONFIG_RAMMTD_ERASESIZE;
  if (nblocks < 1)
    {
      ferr("ERROR: Need to provide at least one full erase block\n");
      kmm_free(priv);
      return NULL;
    }

  /* Perform initialization as necessary. (unsupported methods were
   * nullified by kmm_zalloc).
   */

  priv->mtd.erase  = ram_erase;
  priv->mtd.bread  = ram_bread;
  priv->mtd.bwrite = ram_bwrite;
  priv->mtd.read   = ram_byteread;
#ifdef CONFIG_MTD_BYTE_WRITE
  priv->mtd.write  = ram_bytewrite;
#endif
  priv->mtd.ioctl  = ram_ioctl;
  priv->mtd.name   = "rammtd";

  priv->start      = start;
  priv->nblocks    = nblocks;

  return &priv->mtd;
}