/* Buffering for multi-byte characters.
   Copyright (C) 2025-2026 Free Software Foundation, Inc.

   This file is free software: you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as
   published by the Free Software Foundation; either version 2.1 of the
   License, or (at your option) any later version.

   This file is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

/* Written by Collin Funk.  */

#ifndef _MBBUF_H
#define _MBBUF_H 1

#ifndef _GL_INLINE_HEADER_BEGIN
# error "Please include config.h first."
#endif

#include <stdio.h>
#include <stddef.h>
#include <unistd.h>

#include "fseterr.h"
#include "mcel.h"
#include "idx.h"

_GL_INLINE_HEADER_BEGIN
#ifndef MBBUF_INLINE
# define MBBUF_INLINE _GL_INLINE
#endif

/* End of file.  */
#define MBBUF_EOF UINT32_MAX

/* MBBUF_EOF should not be a valid character.  */
static_assert (MCEL_CHAR_MAX < MBBUF_EOF);

typedef struct
{
  char *buffer;    /* Input buffer.  */
  FILE *fp;        /* Input file stream.  */
  idx_t size;      /* Number of bytes allocated for BUFFER.  */
  idx_t length;    /* Number of bytes with data in BUFFER.  */
  idx_t offset;    /* Current position in BUFFER.  */
  bool eof;        /* Whether at End Of File.  */
} mbbuf_t;

MBBUF_INLINE idx_t
mbbuf_avail (mbbuf_t const *mbbuf)
{
  return mbbuf->length - mbbuf->offset;
}

/* Initialize MBBUF with an allocated BUFFER of SIZE bytes and a file stream
   FP open for reading.  SIZE must be greater than or equal to MCEL_LEN_MAX.
 */
MBBUF_INLINE void
mbbuf_init (mbbuf_t *mbbuf, char *buffer, idx_t size, FILE *fp)
{
  if (size < MCEL_LEN_MAX)
    unreachable ();
  mbbuf->buffer = buffer;
  mbbuf->fp = fp;
  mbbuf->size = size;
  mbbuf->length = 0;
  mbbuf->offset = 0;
  mbbuf->eof = false;
}

/* Fill the input buffer with at least MCEL_LEN_MAX bytes if possible.
   Return the number of bytes available from the current offset.
   At end of file, MBBUF.EOF is set, and zero will eventually be returned.
   Note feof() will _NOT_ be set on the MBBUF.FP.  */
MBBUF_INLINE idx_t
mbbuf_topup (mbbuf_t *mbbuf)
{
  idx_t available = mbbuf_avail (mbbuf);

  if (available < MCEL_LEN_MAX && ! mbbuf->eof)
    {
      idx_t start;
      if (!(0 < available))
        start = 0;
      else
        {
          memmove (mbbuf->buffer, mbbuf->buffer + mbbuf->offset, available);
          start = available;
        }
      ssize_t  read_ret = read (fileno (mbbuf->fp), mbbuf->buffer + start,
                                mbbuf->size - start);
      if (read_ret < 0)
        {
          fseterr (mbbuf->fp);
          mbbuf->eof = true;  /* Avoid any more reads().  */
          mbbuf->length = start;
        }
      else
        {
          mbbuf->eof = read_ret == 0;
          mbbuf->length = read_ret + start;
        }

      mbbuf->offset = 0;
      available = mbbuf_avail (mbbuf);
    }

  return available;
}

/* Fill the input buffer enough to scan the next character if possible.
   Return the number of bytes available from the current offset.  */
MBBUF_INLINE idx_t
mbbuf_fill (mbbuf_t *mbbuf)
{
  idx_t available = mbbuf_avail (mbbuf);

  if (available == 0)
    return mbbuf_topup (mbbuf);

  if (available < MCEL_LEN_MAX && ! mbbuf->eof)
    {
      mcel_t g = mcel_scan (mbbuf->buffer + mbbuf->offset,
                            mbbuf->buffer + mbbuf->length);
      if (g.err)
        return mbbuf_topup (mbbuf);
    }

  return available;
}

/* Consume N bytes from the current buffer.  */
MBBUF_INLINE void
mbbuf_advance (mbbuf_t *mbbuf, idx_t n)
{
  if (mbbuf_avail (mbbuf) < n)
    unreachable ();
  mbbuf->offset += n;
}

/* Get the next character in the buffer, filling it from FP if necessary.
   If an invalid multi-byte character is seen, we assume the program wants to
   fall back to the read byte.  */
MBBUF_INLINE mcel_t
mbbuf_get_char (mbbuf_t *mbbuf)
{
  idx_t available = mbbuf_fill (mbbuf);
  if (available <= 0)
    return (mcel_t) { .ch = MBBUF_EOF };
  mcel_t g = mcel_scan (mbbuf->buffer + mbbuf->offset,
                        mbbuf->buffer + mbbuf->length);
  if (! g.err)
    mbbuf->offset += g.len;
  else
    {
      /* Assume the program will emit the byte, but keep the error flag.  */
      g.ch = (unsigned char) mbbuf->buffer[mbbuf->offset++];
    }
  return g;
}

/* Returns a pointer to the first byte in the previously read character from
   mbbuf_get_char.  */
MBBUF_INLINE char *
mbbuf_char_offset (mbbuf_t *mbbuf, mcel_t g)
{
  if (mbbuf->offset < g.len)
    unreachable ();
  return mbbuf->buffer + (mbbuf->offset - g.len);
}

_GL_INLINE_HEADER_END

#endif
