/*
 * SPDX-License-Identifier: BSD-3-Clause
 *
 * Copyright © 2022 Keith Packard
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "local-stdio.h"

#define __MALL 0x01
#define __MAPP 0x02

struct __file_mem {
    struct __file_ext xfile;
    char             *buf;
    size_t            size;    /* Current size. */
    size_t            bufsize; /* Upper limit on size. */
    size_t            pos;
    uint8_t           mflags;
};

static int
__fmem_put(char c, FILE *f)
{
    struct __file_mem *mf = (struct __file_mem *)f;
    size_t             pos = mf->mflags & __MAPP ? mf->size : mf->pos;
    if ((f->flags & __SWR) == 0) {
        return _FDEV_ERR;
    } else if (pos < mf->bufsize) {
        mf->buf[pos++] = c;
        if (pos > mf->size) {
            mf->size = pos;
            /* When a stream open for update (the mode argument includes '+') or
             * for writing only is successfully written and the write advances
             * the current buffer end position, a null byte shall be written at
             * the new buffer end position if it fits. */
            if (mf->size < mf->bufsize) {
                mf->buf[mf->size] = '\0';
            }
        }
        mf->pos = pos;
        return (unsigned char)c;
    } else {
        return _FDEV_EOF;
    }
}

static int
__fmem_get(FILE *f)
{
    struct __file_mem *mf = (struct __file_mem *)f;
    if ((f->flags & __SRD) == 0) {
        return _FDEV_ERR;
    } else if (mf->pos < mf->size) {
        return (unsigned char)mf->buf[mf->pos++];
    } else {
        return _FDEV_EOF;
    }
}

static int
__fmem_flush(FILE *f)
{
    struct __file_mem *mf = (struct __file_mem *)f;
    if ((f->flags & __SWR) && mf->pos < mf->bufsize) {
        mf->buf[mf->pos] = '\0';
        if (mf->pos > mf->size) {
            mf->size = mf->pos;
        }
    }
    return 0;
}

static off_t
__fmem_seek(FILE *f, off_t pos, int whence)
{
    struct __file_mem *mf = (struct __file_mem *)f;

    switch (whence) {
    case SEEK_SET:
        break;
    case SEEK_CUR:
        pos += mf->pos;
        break;
    case SEEK_END:
        pos += mf->size;
        break;
    default:
        errno = EINVAL;
        return EOF;
    }
    _Static_assert(sizeof(off_t) >= sizeof(size_t), "must avoid truncation");
    if (pos < 0 || (off_t)mf->bufsize < pos)
        return EOF;
    mf->pos = pos;
    return pos;
}

static int
__fmem_close(FILE *f)
{
    struct __file_mem *mf = (struct __file_mem *)f;

    if (mf->mflags & __MALL)
        free(mf->buf);
    else
        __fmem_flush(f);
    free(f);
    return 0;
}

FILE *
fmemopen(void *buf, size_t size, const char *mode)
{
    int                stdio_flags;
    int                open_flags;
    uint8_t            mflags = 0;
    size_t             initial_pos = 0;
    size_t             initial_size;
    struct __file_mem *mf;

    stdio_flags = __stdio_flags(mode, &open_flags);

    if (stdio_flags == 0 || size == 0) {
        errno = EINVAL;
        return NULL;
    }

    /* Allocate file structure and necessary buffers */
    mf = calloc(1, sizeof(struct __file_mem));

    if (mf == NULL)
        return NULL;

    if (buf == NULL) {
        /* POSIX says return EINVAL if: The buf argument is a null pointer and
         * the mode argument does not include a '+' character. */
        if ((stdio_flags & (__SRD | __SWR)) != (__SRD | __SWR)) {
            free(mf);
            errno = EINVAL;
            return NULL;
        }
        buf = malloc(size);
        if (!buf) {
            free(mf);
            errno = ENOMEM;
            return NULL;
        }
        *((char *)buf) = '\0';
        mflags |= __MALL;
    }

    if (open_flags & O_CREAT) {
        if (open_flags & O_APPEND) {
            /* For append the position is set to the first NUL byte or the end. */
            initial_pos = (mflags & __MALL) ? 0 : strnlen(buf, size);
            initial_size = initial_pos;
            mflags |= __MAPP;
        } else {
            initial_size = 0;
            /* w+ mode truncates the buffer, writing NUL */
            if ((open_flags & O_ACCMODE) == O_RDWR)
                *((char *)buf) = '\0';
        }
    } else {
        initial_size = size;
    }

    *mf = (struct __file_mem) {
        .xfile = FDEV_SETUP_EXT(__fmem_put, __fmem_get, __fmem_flush, __fmem_close, __fmem_seek,
                                NULL, stdio_flags),
        .buf = buf,
        .size = initial_size,
        .bufsize = size,
        .pos = initial_pos,
        .mflags = mflags,
    };

    return (FILE *)mf;
}
