/*
 * io.c
 *
 * Copyright (C) 2017 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <unistd.h>
#include "io_priv.h"

static list_entry_t open_files;

FILE *stdin  = NULL;
FILE *stdout = NULL;
FILE *stderr = NULL;

int fileno_unlocked(FILE *stream)
{
    if (stream->fd == -1) errno = EBADF;
    return stream->fd;
}

int fileno(FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = fileno_unlocked(stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

static int __crt_fflush_stream(FILE *stream)
{
    if (!stream->buffer) return EOF;

    if (!(stream->flags & FILE_BUFFER_DIR))
    {
        stream->buffer_start = stream->buffer_end = 0;
        stream->flags &= ~FILE_BUFFER_FULL;
        return 0;
    }

    if (stream->fd == -1) return 0;

    if (stream->buffer_start < stream->buffer_end)
    {
        ssize_t written = write(stream->fd, &stream->buffer[stream->buffer_start], stream->buffer_end - stream->buffer_start);
        if (written < 0) return EOF;

        stream->buffer_start += written;
        stream->buffer_start %= stream->buffer_size;
        if (written) stream->flags &= ~FILE_BUFFER_FULL;
    }
    else
    {
        if (stream->buffer_start < stream->buffer_size)
        {
            ssize_t written = write(stream->fd, &stream->buffer[stream->buffer_start], stream->buffer_size - stream->buffer_start);
            if (written < 0) return EOF;

            stream->buffer_start += written;
            stream->buffer_start %= stream->buffer_size;
            if (written) stream->flags &= ~FILE_BUFFER_FULL;
        }

        if (stream->buffer_end)
        {
            ssize_t written = write(stream->fd, stream->buffer, stream->buffer_end);
            if (written < 0) return EOF;

            stream->buffer_start += written;
            stream->buffer_start %= stream->buffer_size;
            if (written) stream->flags &= ~FILE_BUFFER_FULL;
        }
    }

    return 0;
}

int fflush_unlocked(FILE *stream)
{
    if (stream) return __crt_fflush_stream(stream);

    list_entry_t *entry;
    int result = 0;

    for (entry = open_files.next; entry != &open_files; entry = entry->next)
    {
        if (__crt_fflush_stream(CONTAINER_OF(entry, struct __crt_file, link)) == EOF)
        {
            result = EOF;
        }
    }

    return result;
}

int fflush(FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = fflush_unlocked(stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

int setvbuf(FILE *stream, char *buf, int mode, size_t size)
{
    int ret = EOF;
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);

    switch (mode)
    {
    case _IONBF:
        if (fflush_unlocked(stream) == 0)
        {
            stream->buffer = NULL;
            stream->buffer_size = 0;
            ret = 0;
        }
        break;

    case _IOLBF:
        if (!buf || fflush_unlocked(stream) == 0)
        {
            if (buf)
            {
                stream->buffer = buf;
                stream->buffer_size = size;
            }

            stream->flags |= FILE_BUFFER_LINE;
            ret = 0;
        }
        break;

    case _IOFBF:
        if (!buf || fflush_unlocked(stream) == 0)
        {
            if (buf)
            {
                stream->buffer = buf;
                stream->buffer_size = size;
            }

            stream->flags &= ~FILE_BUFFER_LINE;
            ret = 0;
        }
        break;
    }

    syscall_release_mutex(stream->mutex);
    return ret;
}

void setbuf(FILE *stream, char *buf)
{
    setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
}

void setbuffer(FILE *stream, char *buf, size_t size)
{
    setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size);
}

void setlinebuf(FILE *stream)
{
    setvbuf(stream, NULL, _IOLBF, 0);
}

inline int fgetc_unlocked(FILE *stream)
{
    char c;

    if (!(stream->flags & FILE_READ))
    {
        errno = EPERM;
        return EOF;
    }

    if (stream->buffer)
    {
        if (stream->flags & FILE_BUFFER_DIR)
        {
            fflush_unlocked(stream);
            stream->flags &= ~FILE_BUFFER_DIR;
        }

        if (stream->buffer_start == stream->buffer_end && !(stream->flags & FILE_BUFFER_FULL))
        {
            if (stream->fd == -1) return EOF;

            if (stream->buffer_end < stream->buffer_size)
            {
                ssize_t amount = read(stream->fd, &stream->buffer[stream->buffer_end], stream->buffer_size - stream->buffer_end);
                if (amount < 0) return EOF;

                stream->buffer_end += amount;
                stream->buffer_end %= stream->buffer_size;
                if (amount && (stream->buffer_start == stream->buffer_end)) stream->flags |= FILE_BUFFER_FULL;
            }

            if (stream->buffer_end < stream->buffer_start)
            {
                ssize_t amount = read(stream->fd, &stream->buffer[stream->buffer_end], stream->buffer_start - stream->buffer_end);
                if (amount < 0) return EOF;

                stream->buffer_end += amount;
                stream->buffer_end %= stream->buffer_size;
                if (amount && (stream->buffer_start == stream->buffer_end)) stream->flags |= FILE_BUFFER_FULL;
            }
        }

        c = stream->buffer[stream->buffer_start];
        stream->buffer_start++;
        stream->buffer_end %= stream->buffer_size;

        stream->flags &= ~FILE_BUFFER_FULL;
    }
    else
    {
        if (read(stream->fd, &c, 1) != 1) c = EOF;
    }

    return (int)((unsigned int)c);
}

int fgetc(FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = fgetc_unlocked(stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

inline int fputc_unlocked(int c, FILE *stream)
{
    if (!(stream->flags & FILE_WRITE))
    {
        errno = EPERM;
        return EOF;
    }

    if (stream->buffer)
    {
        if (!(stream->flags & FILE_BUFFER_DIR))
        {
            fflush_unlocked(stream);
            stream->flags |= FILE_BUFFER_DIR;
        }

        if (stream->flags & FILE_BUFFER_FULL)
        {
            errno = ENOSPC;
            return EOF;
        }

        stream->buffer[stream->buffer_end] = (uint8_t)c;
        stream->buffer_end++;
        stream->buffer_end %= stream->buffer_size;

        if (stream->buffer_start == stream->buffer_end)
        {
            stream->flags |= FILE_BUFFER_FULL;
        }

        if (stream->fd != -1 && ((stream->flags & FILE_BUFFER_FULL) || ((stream->flags & FILE_BUFFER_LINE) && (char)c == '\n')))
        {
            fflush_unlocked(stream);
        }
    }
    else
    {
        if (write(stream->fd, &c, 1) != 1) return EOF;
    }

    return 0;
}

int fputc(int c, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = fputc_unlocked(c, stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

char *fgets_unlocked(char *s, int size, FILE *stream)
{
    int c;
    char *ptr = s;

    while (size-- > 0 && (c = fgetc_unlocked(stream)) != EOF)
    {
        *ptr++ = (char)c;
        if (c == '\n') break;
    }

    return (s != ptr) ? s : NULL;
}

char *fgets(char *s, int size, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    char *ret = fgets_unlocked(s, size, stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

int fputs_unlocked(const char *s, FILE *stream)
{
    const char *ptr = s;
    while (*ptr) if (fputc_unlocked(*ptr++, stream) == EOF) return EOF;
    return 0;
}

int fputs(const char *s, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = fputs_unlocked(s, stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

int ungetc(int c, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);

    if (stream->flags & FILE_BUFFER_DIR)
    {
        fflush_unlocked(stream);
        stream->flags &= ~FILE_BUFFER_DIR;
    }

    if (stream->buffer_start == 0) stream->buffer_start = stream->buffer_size;
    stream->buffer_start--;

    stream->buffer[stream->buffer_start] = (uint8_t)c;

    syscall_release_mutex(stream->mutex);
    return c;
}

char *gets(char *s)
{
    char *ptr = s;
    int c;
    while ((c = getchar()) != EOF) *ptr++ = (char)c;
    return ptr != s ? s : NULL;
}

int puts(const char *s)
{
    if (fputs(s, stdout) == EOF) return EOF;
    if (fputc('\n', stdout) == EOF) return EOF;
    return 0;
}

FILE *fdopen(int fd, const char *mode)
{
    FILE *stream = (FILE*)malloc(sizeof(FILE) + BUFSIZ);

    if (stream == NULL)
    {
        errno = ENOMEM;
        return NULL;
    }

    sysret_t status = syscall_create_mutex(NULL, TRUE, &stream->mutex);
    if (status != ERR_SUCCESS)
    {
        free(stream);
        errno = __crt_translate_error(status);
        return NULL;
    }

    stream->flags = 0;

    const char *ptr;
    for (ptr = mode; *ptr; ptr++)
    {
        switch (*ptr)
        {
        case 'r':
            stream->flags |= FILE_READ;
            break;
        case 'w':
            stream->flags |= FILE_WRITE;
            break;
        case 'a':
            stream->flags |= FILE_APPEND;
            break;
        case '+':
            stream->flags |= FILE_READ | FILE_WRITE;
            break;
        }
    }

    stream->fd = fd;

    char *buffer = (char*)((uintptr_t)stream + sizeof(FILE));
    setbuf(stream, buffer);
    return stream;
}

FILE *fmemopen(void *buf, size_t size, const char *mode)
{
    FILE *stream = (FILE*)malloc(sizeof(FILE));
    if (stream == NULL)
    {
        errno = ENOMEM;
        return NULL;
    }

    sysret_t status = syscall_create_mutex(NULL, TRUE, &stream->mutex);
    if (status != ERR_SUCCESS)
    {
        free(stream);
        errno = __crt_translate_error(status);
        return NULL;
    }

    stream->flags = 0;

    const char *ptr;
    for (ptr = mode; *ptr; ptr++)
    {
        switch (*ptr)
        {
        case 'r':
            stream->flags |= FILE_READ;
            break;
        case 'w':
            stream->flags |= FILE_WRITE;
            break;
        case 'a':
            stream->flags |= FILE_APPEND;
            break;
        case '+':
            stream->flags |= FILE_READ | FILE_WRITE;
            break;
        }
    }

    stream->fd = -1;
    stream->buffer = buf;
    stream->buffer_size = size;
    stream->buffer_start = stream->buffer_end = 0;

    list_append(&open_files, &stream->link);
    return stream;
}

FILE *fopen(const char *pathname, const char *mode)
{
    int open_flags = 0;

    const char *ptr;
    for (ptr = mode; *ptr; ptr++)
    {
        switch (*ptr)
        {
        case 'r':
            open_flags = (open_flags & ~3) | O_RDONLY;
            break;
        case 'w':
            open_flags = (open_flags & ~3) | O_WRONLY;
            break;
        case 'a':
            open_flags |= O_APPEND;
            break;
        case '+':
            open_flags = (open_flags & ~3) | O_RDWR;
            break;
        }
    }

    int fd = open(pathname, open_flags);
    if (fd < 0) return NULL;

    FILE *stream = fdopen(fd, mode);
    if (!stream) close(fd);
    return NULL;
}

int fclose(FILE *stream)
{
    fflush(stream);
    list_remove(&stream->link);
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);

    if (stream->fd) close(stream->fd);

    syscall_close_object(stream->mutex);
    free(stream);
    return 0;
}
