/****************************************************************************
 * libs/libc/stdio/lib_fmemopen.c
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
#include <errno.h>

#include "libc.h"

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

struct fmemopen_cookie_s
{
  FAR char *buf;    /* Memory buffer */
  off_t pos;        /* Current position in the buffer */
  off_t end;        /* End buffer position */
  size_t size;      /* Buffer size */
  bool custom;      /* True if custom buffer is used */
};

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

/****************************************************************************
 * Name: fmemopen_read
 ****************************************************************************/

static ssize_t fmemopen_read(FAR void *c, FAR char *buf, size_t size)
{
  FAR struct fmemopen_cookie_s *fmemopen_cookie =
    (FAR struct fmemopen_cookie_s *)c;
  if (fmemopen_cookie->pos + size > fmemopen_cookie->end)
    {
      size = fmemopen_cookie->end - fmemopen_cookie->pos;
    }

  memcpy(buf, fmemopen_cookie->buf + fmemopen_cookie->pos, size);

  fmemopen_cookie->pos += size;
  return size;
}

/****************************************************************************
 * Name: fmemopen_write
 ****************************************************************************/

static ssize_t fmemopen_write(FAR void *c, FAR const char *buf, size_t size)
{
  FAR struct fmemopen_cookie_s *fmemopen_cookie =
    (FAR struct fmemopen_cookie_s *)c;
  if (size + fmemopen_cookie->pos > fmemopen_cookie->size)
    {
      size = fmemopen_cookie->size - fmemopen_cookie->pos;
    }

  memcpy(fmemopen_cookie->buf + fmemopen_cookie->pos, buf, size);

  fmemopen_cookie->pos += size;
  if (fmemopen_cookie->pos > fmemopen_cookie->end)
    {
      fmemopen_cookie->end = fmemopen_cookie->pos;
    }

  /* POSIX states that NULL byte shall be written at the current position
   * or end of the buffer.
   */

  if (fmemopen_cookie->pos < fmemopen_cookie->size &&
      fmemopen_cookie->buf[fmemopen_cookie->pos - 1] != '\0')
    {
      fmemopen_cookie->buf[fmemopen_cookie->pos] = '\0';
    }

  return size;
}

/****************************************************************************
 * Name: fmemopen_seek
 ****************************************************************************/

static off_t fmemopen_seek(FAR void *c, FAR off_t *offset, int whence)
{
  FAR struct fmemopen_cookie_s *fmemopen_cookie =
    (FAR struct fmemopen_cookie_s *)c;
  off_t new_offset;

  switch (whence)
    {
      case SEEK_SET:
        new_offset = *offset;
        break;
      case SEEK_END:
        new_offset = fmemopen_cookie->end + *offset;
        break;
      case SEEK_CUR:
        new_offset = fmemopen_cookie->pos + *offset;
        break;
      default:
        set_errno(ENOTSUP);
        return -1;
    }

  /* Seek to negative value or value larger than maximum size shall fail. */

  if (new_offset < 0 || new_offset > fmemopen_cookie->end)
    {
      set_errno(EINVAL);
      return -1;
    }

  fmemopen_cookie->pos = new_offset;
  *offset = new_offset;
  return new_offset;
}

/****************************************************************************
 * Name: fmemopen_close
 ****************************************************************************/

static int fmemopen_close(FAR void *c)
{
  FAR struct fmemopen_cookie_s *fmemopen_cookie =
    (FAR struct fmemopen_cookie_s *)c;
  if (fmemopen_cookie->custom)
    {
      lib_free(fmemopen_cookie->buf);
    }

  lib_free(fmemopen_cookie);
  return 0;
}

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

/****************************************************************************
 * Name: fmemopen
 ****************************************************************************/

FAR FILE *fmemopen(FAR void *buf, size_t size, FAR const char *mode)
{
  cookie_io_functions_t fmemopen_io;
  FAR struct fmemopen_cookie_s *fmemopen_cookie;
  FAR FILE *filep;
  int oflags;

  fmemopen_cookie = lib_zalloc(sizeof(struct fmemopen_cookie_s));
  if (fmemopen_cookie == NULL)
    {
      set_errno(ENOMEM);
      return NULL;
    }

  oflags = lib_mode2oflags(mode);

  if (buf == NULL)
    {
      /* POSIX standard states:
       * Because this feature is only useful when the stream is opened for
       * updating (because there is no way to get a pointer to the buffer)
       * the fmemopen() call may fail if the mode argument does not
       * include a '+'.
       */

      if ((oflags & O_RDWR) != O_RDWR)
        {
          lib_free(fmemopen_cookie);
          set_errno(EINVAL);
          return NULL;
        }

      /* Buf argument is NULL pointer and mode is correct. This buffer
       * will be freed when stream is closed so we have to keep the
       * information in fmemopen_cookie->custom.
       */

      fmemopen_cookie->custom = true;
      fmemopen_cookie->buf    = lib_zalloc(size);
      if (fmemopen_cookie->buf == NULL)
        {
          lib_free(fmemopen_cookie);
          set_errno(ENOMEM);
          return NULL;
        }

      /* If buf is a null pointer, the initial position shall always be set
       * to the beginning of the buffer.
       */

      fmemopen_cookie->buf[0] = '\0';
    }
  else
    {
      /* Buffer was already allocated by the user. */

      fmemopen_cookie->custom = false;
      fmemopen_cookie->buf    = buf;
    }

  fmemopen_cookie->size = size;
  fmemopen_cookie->pos  = 0;

  /* For modes w and w+ the initial size shall be zero. */

  if ((oflags & O_TRUNC) != 0)
    {
      fmemopen_cookie->end = 0;
      fmemopen_cookie->buf[0] = '\0';
    }

  /* For modes r and r+ the size shall be set to the value given
   * by the size argument.
   */

  if ((oflags & O_RDWR) == O_RDOK)
    {
      fmemopen_cookie->end = size;
    }

  /* For modes a and a+ the initial size shall be:
   *  - Zero, if buf is a null pointer
   *  - The position of the first null byte in the buffer, if one is foun
   *  - The value of the size argument, if buf is not a null pointer and
   *    no null byte is found
   */

  if ((oflags & O_APPEND) != 0)
    {
      fmemopen_cookie->pos = fmemopen_cookie->end =
          strnlen(fmemopen_cookie->buf, fmemopen_cookie->size);
    }

  /* Assign fmemopen callbacks. */

  fmemopen_io.read  = fmemopen_read;
  fmemopen_io.write = fmemopen_write;
  fmemopen_io.seek  = fmemopen_seek;
  fmemopen_io.close = fmemopen_close;

  /* Let fopencookie do the rest. */

  filep = fopencookie(fmemopen_cookie, mode, fmemopen_io);
  if (filep == NULL)
    {
      if (fmemopen_cookie->custom)
        {
          lib_free(fmemopen_cookie->buf);
        }

      lib_free(fmemopen_cookie);
      return NULL;
    }

  return filep;
}
