/// @file fileio.c
///
/// Buffered reading/writing to a file. Unlike fileio.c this is not dealing with
/// Nvim structures for buffer, with autocommands, etc: just fopen/fread/fwrite
/// replacement.

#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <uv.h>

#include "auto/config.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/log.h"
#include "nvim/macros_defs.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/os/fileio.h"
#include "nvim/os/fs.h"
#include "nvim/os/os_defs.h"
#include "nvim/rbuffer.h"
#include "nvim/rbuffer_defs.h"
#include "nvim/types_defs.h"

#ifdef HAVE_SYS_UIO_H
# include <sys/uio.h>
#endif

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/fileio.c.generated.h"
#endif

/// Open file
///
/// @param[out]  ret_fp  Address where information needed for reading from or
///                      writing to a file is saved
/// @param[in]  fname  File name to open.
/// @param[in]  flags  Flags, @see FileOpenFlags. Currently reading from and
///                    writing to the file at once is not supported, so either
///                    kFileWriteOnly or kFileReadOnly is required.
/// @param[in]  mode  Permissions for the newly created file (ignored if flags
///                   does not have kFileCreate\*).
///
/// @return Error code, or 0 on success. @see os_strerror()
int file_open(FileDescriptor *const ret_fp, const char *const fname, const int flags,
              const int mode)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
  int os_open_flags = 0;
  TriState wr = kNone;
#define FLAG(flags, flag, fcntl_flags, wrval, cond) \
  do { \
    if (flags & flag) { \
      os_open_flags |= fcntl_flags; \
      assert(cond); \
      if (wrval != kNone) { \
        wr = wrval; \
      } \
    } \
  } while (0)
  FLAG(flags, kFileWriteOnly, O_WRONLY, kTrue, true);
  FLAG(flags, kFileCreateOnly, O_CREAT|O_EXCL|O_WRONLY, kTrue, true);
  FLAG(flags, kFileCreate, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly));
  FLAG(flags, kFileTruncate, O_TRUNC|O_WRONLY, kTrue,
       !(flags & kFileCreateOnly));
  FLAG(flags, kFileAppend, O_APPEND|O_WRONLY, kTrue,
       !(flags & kFileCreateOnly));
  FLAG(flags, kFileReadOnly, O_RDONLY, kFalse, wr != kTrue);
#ifdef O_NOFOLLOW
  FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true);
  FLAG(flags, kFileMkDir, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly));
#endif
#undef FLAG
  // wr is used for kFileReadOnly flag, but on
  // QB:neovim-qb-slave-ubuntu-12-04-64bit it still errors out with
  // `error: variable ‘wr’ set but not used [-Werror=unused-but-set-variable]`
  (void)wr;

  if (flags & kFileMkDir) {
    int mkdir_ret = os_file_mkdir((char *)fname, 0755);
    if (mkdir_ret < 0) {
      return mkdir_ret;
    }
  }

  const int fd = os_open(fname, os_open_flags, mode);

  if (fd < 0) {
    return fd;
  }
  return file_open_fd(ret_fp, fd, flags);
}

/// Wrap file descriptor with FileDescriptor structure
///
/// @warning File descriptor wrapped like this must not be accessed by other
///          means.
///
/// @param[out]  ret_fp  Address where information needed for reading from or
///                      writing to a file is saved
/// @param[in]  fd  File descriptor to wrap.
/// @param[in]  flags  Flags, @see FileOpenFlags. Currently reading from and
///                    writing to the file at once is not supported, so either
///                    FILE_WRITE_ONLY or FILE_READ_ONLY is required.
///
/// @return Error code (@see os_strerror()) or 0. Currently always returns 0.
int file_open_fd(FileDescriptor *const ret_fp, const int fd, const int flags)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
  ret_fp->wr = !!(flags & (kFileCreate
                           |kFileCreateOnly
                           |kFileTruncate
                           |kFileAppend
                           |kFileWriteOnly));
  ret_fp->non_blocking = !!(flags & kFileNonBlocking);
  // Non-blocking writes not supported currently.
  assert(!ret_fp->wr || !ret_fp->non_blocking);
  ret_fp->fd = fd;
  ret_fp->eof = false;
  ret_fp->rv = rbuffer_new(kRWBufferSize);
  ret_fp->_error = 0;
  if (ret_fp->wr) {
    ret_fp->rv->data = ret_fp;
    ret_fp->rv->full_cb = (rbuffer_callback)&file_rb_write_full_cb;
  }
  return 0;
}

/// Opens standard input as a FileDescriptor.
int file_open_stdin(FileDescriptor *fp)
  FUNC_ATTR_WARN_UNUSED_RESULT
{
  int error = file_open_fd(fp, os_open_stdin_fd(), kFileReadOnly|kFileNonBlocking);
  if (error != 0) {
    ELOG("failed to open stdin: %s", os_strerror(error));
  }
  return error;
}

/// Close file and free its buffer
///
/// @param[in,out]  fp  File to close.
/// @param[in]  do_fsync  If true, use fsync() to write changes to disk.
///
/// @return 0 or error code.
int file_close(FileDescriptor *const fp, const bool do_fsync)
  FUNC_ATTR_NONNULL_ALL
{
  const int flush_error = (do_fsync ? file_fsync(fp) : file_flush(fp));
  const int close_error = os_close(fp->fd);
  rbuffer_free(fp->rv);
  if (close_error != 0) {
    return close_error;
  }
  return flush_error;
}

/// Flush file modifications to disk
///
/// @param[in,out]  fp  File to work with.
///
/// @return 0 or error code.
int file_flush(FileDescriptor *const fp)
  FUNC_ATTR_NONNULL_ALL
{
  if (!fp->wr) {
    return 0;
  }
  file_rb_write_full_cb(fp->rv, fp);
  const int error = fp->_error;
  fp->_error = 0;
  return error;
}

/// Flush file modifications to disk and run fsync()
///
/// @param[in,out]  fp  File to work with.
///
/// @return 0 or error code.
int file_fsync(FileDescriptor *const fp)
  FUNC_ATTR_NONNULL_ALL
{
  if (!fp->wr) {
    return 0;
  }
  const int flush_error = file_flush(fp);
  if (flush_error != 0) {
    return flush_error;
  }
  const int fsync_error = os_fsync(fp->fd);
  if (fsync_error != UV_EINVAL
      && fsync_error != UV_EROFS
      // fsync not supported on this storage.
      && fsync_error != UV_ENOTSUP) {
    return fsync_error;
  }
  return 0;
}

/// Buffer used for writing
///
/// Like IObuff, but allows file_\* callers not to care about spoiling it.
static char writebuf[kRWBufferSize];

/// Function run when RBuffer is full when writing to a file
///
/// Actually does writing to the file, may also be invoked directly.
///
/// @param[in,out]  rv  RBuffer instance used.
/// @param[in,out]  fp  File to work with.
static void file_rb_write_full_cb(RBuffer *const rv, void *const fp_in)
  FUNC_ATTR_NONNULL_ALL
{
  FileDescriptor *const fp = fp_in;
  assert(fp->wr);
  assert(rv->data == (void *)fp);
  if (rbuffer_size(rv) == 0) {
    return;
  }
  const size_t read_bytes = rbuffer_read(rv, writebuf, kRWBufferSize);
  const ptrdiff_t wres = os_write(fp->fd, writebuf, read_bytes,
                                  fp->non_blocking);
  if (wres != (ptrdiff_t)read_bytes) {
    if (wres >= 0) {
      fp->_error = UV_EIO;
    } else {
      fp->_error = (int)wres;
    }
  }
}

/// Read from file
///
/// @param[in,out]  fp  File to work with.
/// @param[out]  ret_buf  Buffer to read to. Must not be NULL.
/// @param[in]  size  Number of bytes to read. Buffer must have at least ret_buf
///                   bytes.
///
/// @return error_code (< 0) or number of bytes read.
ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf, const size_t size)
  FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
  assert(!fp->wr);
  char *buf = ret_buf;
  size_t read_remaining = size;
  RBuffer *const rv = fp->rv;
  bool called_read = false;
  while (read_remaining) {
    const size_t rv_size = rbuffer_size(rv);
    if (rv_size > 0) {
      const size_t rsize = rbuffer_read(rv, buf, MIN(rv_size, read_remaining));
      buf += rsize;
      read_remaining -= rsize;
    }
    if (fp->eof
        // Allow only at most one os_read[v] call.
        || (called_read && fp->non_blocking)) {
      break;
    }
    if (read_remaining) {
      assert(rbuffer_size(rv) == 0);
      rbuffer_reset(rv);
#ifdef HAVE_READV
      // If there is readv() syscall, then take an opportunity to populate
      // both target buffer and RBuffer at once, …
      size_t write_count;
      struct iovec iov[] = {
        { .iov_base = buf, .iov_len = read_remaining },
        { .iov_base = rbuffer_write_ptr(rv, &write_count),
          .iov_len = kRWBufferSize },
      };
      assert(write_count == kRWBufferSize);
      const ptrdiff_t r_ret = os_readv(fp->fd, &fp->eof, iov,
                                       ARRAY_SIZE(iov), fp->non_blocking);
      if (r_ret > 0) {
        if (r_ret > (ptrdiff_t)read_remaining) {
          rbuffer_produced(rv, (size_t)(r_ret - (ptrdiff_t)read_remaining));
          read_remaining = 0;
        } else {
          buf += (size_t)r_ret;
          read_remaining -= (size_t)r_ret;
        }
      } else if (r_ret < 0) {
        return r_ret;
      }
#else
      if (read_remaining >= kRWBufferSize) {
        // …otherwise leave RBuffer empty and populate only target buffer,
        // because filtering information through rbuffer will be more syscalls.
        const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof, buf, read_remaining,
                                        fp->non_blocking);
        if (r_ret >= 0) {
          read_remaining -= (size_t)r_ret;
          return (ptrdiff_t)(size - read_remaining);
        } else if (r_ret < 0) {
          return r_ret;
        }
      } else {
        size_t write_count;
        const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof,
                                        rbuffer_write_ptr(rv, &write_count),
                                        kRWBufferSize, fp->non_blocking);
        assert(write_count == kRWBufferSize);
        if (r_ret > 0) {
          rbuffer_produced(rv, (size_t)r_ret);
        } else if (r_ret < 0) {
          return r_ret;
        }
      }
#endif
      called_read = true;
    }
  }
  return (ptrdiff_t)(size - read_remaining);
}

/// Write to a file
///
/// @param[in]  fd  File descriptor to write to.
/// @param[in]  buf  Data to write. May be NULL if size is zero.
/// @param[in]  size  Amount of bytes to write.
///
/// @return Number of bytes written or libuv error code (< 0).
ptrdiff_t file_write(FileDescriptor *const fp, const char *const buf, const size_t size)
  FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(1)
{
  assert(fp->wr);
  const size_t written = rbuffer_write(fp->rv, buf, size);
  if (fp->_error != 0) {
    const int error = fp->_error;
    fp->_error = 0;
    return error;
  } else if (written != size) {
    return UV_EIO;
  }
  return (ptrdiff_t)written;
}

/// Buffer used for skipping. Its contents is undefined and should never be
/// used.
static char skipbuf[kRWBufferSize];

/// Skip some bytes
///
/// This is like `fseek(fp, size, SEEK_CUR)`, but actual implementation simply
/// reads to a buffer and discards the result.
ptrdiff_t file_skip(FileDescriptor *const fp, const size_t size)
  FUNC_ATTR_NONNULL_ALL
{
  assert(!fp->wr);
  size_t read_bytes = 0;
  do {
    const ptrdiff_t new_read_bytes =
      file_read(fp, skipbuf, MIN(size - read_bytes, sizeof(skipbuf)));
    if (new_read_bytes < 0) {
      return new_read_bytes;
    } else if (new_read_bytes == 0) {
      break;
    }
    read_bytes += (size_t)new_read_bytes;
  } while (read_bytes < size && !file_eof(fp));

  return (ptrdiff_t)read_bytes;
}

/// Print error which occurs when failing to write msgpack data
///
/// @param[in]  error  Error code of the error to print.
///
/// @return -1 (error return for msgpack_packer callbacks).
int msgpack_file_write_error(const int error)
{
  semsg(_("E5420: Failed to write to file: %s"), os_strerror(error));
  return -1;
}
