#include <bits/errno.h>
#include <bits/fcntl.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED 0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK 0x8000

struct __fbuf {
    char* base;
    char* ptr; /* current pointer */
    size_t size;
};

struct __rwbuf {
    __fbuf_t rbuf;
    __fbuf_t wbuf;
    char* base;
    size_t size;
};

struct __file {
    int _flags; /* flags, below; this FILE is free if 0 */
    int _file; /* fileno, if Unix descriptor, else -1 */
    size_t _r; /* read space left */
    size_t _w; /* write space left */
    __rwbuf_t _bf; /* rw buffer */
    int _ungotc; /* ungot char. If spot is empty, it equals to UNGOTC_EMPTY */
};

static FILE _stdstreams[3];
FILE* stdin = &_stdstreams[0];
FILE* stdout = &_stdstreams[1];
FILE* stderr = &_stdstreams[2];

/* Static functions */
static inline int _can_read(FILE* file);
static inline int _can_write(FILE* file);
static inline int _can_use_buffer(FILE* file);
static int _parse_mode(const char* mode, mode_t* flags);

/* Buffer */
static inline int _free_buf(FILE* stream);
static size_t _do_system_write(const void* ptr, size_t size, FILE* stream);
static int _resize_buf(FILE* stream, size_t size);
static ssize_t _flush_wbuf(FILE* stream);
static void _split_rwbuf(FILE* stream);
static int _resize_buf(FILE* stream, size_t size);

/* Stream */
static int _init_stream(FILE* file);
static int _init_file_with_fd(FILE* file, int fd);
static int _open_file(FILE* file, const char* path, const char* mode);
static FILE* _fopen_internal(const char* path, const char* mode);

/* Read/write */
static size_t _do_system_read(char* ptr, size_t size, FILE* stream);
static size_t _do_system_write(const void* ptr, size_t size, FILE* stream);
static size_t _fread_internal(char* ptr, size_t size, FILE* stream);
static size_t _fwrite_internal(const void* ptr, size_t size, FILE* stream);

/* Public functions */

FILE* fopen(const char* path, const char* mode)
{
    if (!path || !mode)
        return NULL;

    return _fopen_internal(path, mode);
}

int fclose(FILE* stream)
{
    int res;

    /* Flush & close the stream, and then free any allocated memory. */
    fflush(stream);
    res = close(stream->_file);

    if (res == -EBADF || res == -EFAULT)
        return EOF;

    _free_buf(stream);
    free(stream);

    return 0;
}

size_t fread(void* ptr, size_t size, size_t count, FILE* stream)
{
    if (!ptr || !stream)
        return 0;

    return _fread_internal(ptr, size * count, stream);
}

size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream)
{
    if (!ptr) {
        set_errno(EINVAL);
        return 0;
    }

    if (!stream) {
        set_errno(EINVAL);
        return 0;
    }

    return _fwrite_internal(ptr, size * count, stream);
}

static void _drop_rbuf(FILE* stream)
{
    stream->_r = 0;
    stream->_ungotc = UNGOTC_EMPTY;
}

int fseek(FILE* stream, uint32_t offset, int origin)
{
    if (!stream) {
        set_errno(EINVAL);
        return 0;
    }
    fflush(stream);
    _drop_rbuf(stream);

    return lseek(stream->_file, offset, origin);
}

int fputc(int c, FILE* stream)
{
    int res = fwrite(&c, 1, 1, stream);
    if (!res)
        return -errno;

    return c;
}

int putc(int c, FILE* stream)
{
    return fputc(c, stream);
}

int putchar(int c)
{
    return fputc(c, stdout);
}

long ftell(FILE* stream)
{
    if (!stream) {
        set_errno(EINVAL);
        return 0;
    }

    fflush(stream);
    _drop_rbuf(stream);

    return lseek(stream->_file, 0, SEEK_CUR);
}

int fputs(const char* s, FILE* stream)
{
    size_t len = strlen(s);

    int res = fwrite(s, len, 1, stream);

    if (!res)
        return -errno;

    return res;
}

int puts(const char* s)
{
    return fputs(s, stdout);
}

int fgetc(FILE* stream)
{
    char c;

    if (fread(&c, 1, 1, stream) != 1)
        return EOF;

    return c;
}

int getc(FILE* stream)
{
    return fgetc(stream);
}

int getchar()
{
    return fgetc(stdin);
}

int ungetc(int c, FILE* stream)
{
    if (c == EOF)
        return EOF;

    if (!stream) {
        set_errno(EINVAL);
        return EOF;
    }

    if (stream->_ungotc != UNGOTC_EMPTY) {
        set_errno(EBUSY);
        return EOF;
    }

    stream->_flags &= ~_IO_EOF_SEEN;
    stream->_ungotc = c;
    return c;
}

char* fgets(char* s, int size, FILE* stream)
{
    unsigned int rd = 0;
    char c;

    if (!stream) {
        set_errno(EINVAL);
        return NULL;
    }

    /* We need to flush the stdout and stderr streams before reading. */
    fflush(stdout);
    fflush(stderr);

    while (c != '\n' && rd < size) {
        if ((c = fgetc(stream)) < 0) {
            if (rd == 0) {
                return NULL;
            }
            // EOF should be set
            s[rd] = '\0';
            break;
        }
        s[rd++] = c;
    }

    return s;
}

char* gets(char* str)
{
    // Currently the max size of gets is 4096.
    return fgets(str, 4096, stdout);
}

int setvbuf(FILE* stream, char* buf, int mode, size_t size)
{
    if (!stream) {
        set_errno(EINVAL);
        return -1;
    }

    if (mode != _IONBF && mode != _IOLBF && mode != _IOFBF) {
        set_errno(EINVAL);
        return -1;
    }

    /* Clear the buffer type flags and reset it. */

    stream->_flags &= ~(int)(_IO_UNBUFFERED | _IO_LINE_BUF);
    if (mode & _IOLBF)
        stream->_flags |= _IO_LINE_BUF;

    if (mode & _IONBF)
        stream->_flags |= _IO_UNBUFFERED;

    _flush_wbuf(stream);

    if (!_can_use_buffer(stream))
        return _free_buf(stream);

    if (!buf)
        return _resize_buf(stream, size);

    stream->_bf.base = buf;
    stream->_bf.size = size;
    _split_rwbuf(stream);

    return 0;
}

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

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

int fflush(FILE* stream)
{
    if (!stream)
        return -EBADF;

    return _flush_wbuf(stream);
}

int feof(FILE* stream)
{
    return stream->_flags & _IO_EOF_SEEN;
}

int __stream_info(FILE* stream)
{
    static const char* names[] = { "(STDIN) ", "(STDOUT) ", "(STDERR) " };
    char rwinfo[4] = "-/-";
    __fbuf_t *rbuf, *wbuf;
    const char* name;

    if (!stream)
        return 1;

    if (stream->_file >= 0 && stream->_file <= 2)
        name = names[stream->_file];

    if (_can_read(stream)) {
        rwinfo[0] = 'r';
        rbuf = &stream->_bf.rbuf;
    }

    if (_can_write(stream)) {
        rwinfo[2] = 'w';
        wbuf = &stream->_bf.wbuf;
    }

    printf("__stream_info():\n");
    printf("  fd=%d %sflags=%s\n", stream->_file, name, rwinfo);
    printf("  ungotc=%s val=%x\n", stream->_ungotc == UNGOTC_EMPTY ? "False" : "True", stream->_ungotc);

    if (_can_read(stream)) {
        printf("  read space left=%u\n", stream->_r);
        printf("  rbuf.base=%x rbuf.size=%u rbuf.ptr=%x\n", (size_t)rbuf->base,
            rbuf->size, (size_t)rbuf->ptr);
    }

    if (_can_write(stream)) {
        printf("  write space left=%u\n", stream->_w);
        printf("  wbuf.base=%x wbuf.size=%u wbuf.ptr=%x\n", (size_t)wbuf->base,
            wbuf->size, (size_t)wbuf->ptr);
    }

    printf("  rwbuf.base=%x rwbuf.size=%u\n", (size_t)stream->_bf.base,
        stream->_bf.size);

    return 0;
}

int _stdio_init()
{
    _init_file_with_fd(stdin, STDIN);
    _init_file_with_fd(stdout, STDOUT);
    _init_file_with_fd(stderr, STDERR);
    setbuf(stderr, NULL);
    return 0;
}

int _stdio_deinit()
{
    // FIXME
    _flush_wbuf(stdout);
    return 0;
}

/* Static functions */

static inline int _can_read(FILE* file)
{
    return (file->_flags & _IO_NO_READS) == 0;
}

static inline int _can_write(FILE* file)
{
    return (file->_flags & _IO_NO_WRITES) == 0;
}

static inline int _can_use_buffer(FILE* file)
{
    return (file->_flags & _IO_UNBUFFERED) == 0;
}

/* Because this checks the first and second character only, the possible
   combinations are: r, w, a, r+ and w+. */
static int _parse_mode(const char* mode, mode_t* flags)
{
    int has_plus, len;

    if (!(len = strlen(mode)))
        return 0;

    *flags = 0;
    if (len > 1 && mode[1] == '+')
        has_plus = 1;

    switch (mode[0]) {
    case 'r':
        *flags = has_plus ? O_RDONLY : O_RDWR;
        return 0;

    case 'w':
        *flags = has_plus ? (O_WRONLY | O_CREAT) : (O_RDWR | O_CREAT);
        return 0;

    case 'a':
        *flags = O_APPEND | O_CREAT;
        return 0;

        /* TODO: Add binary mode when the rest will support such option. */

    default:
        return -1;
    }

    return -1;
}

/* Buffer */

static inline int _free_buf(FILE* stream)
{
    /* Don't free the buffer if the user provided one with setvbuf. */
    if (stream->_flags & _IO_USER_BUF)
        return 0;

    if (stream->_bf.base)
        free(stream->_bf.base);

    return 0;
}

static void _split_rwbuf(FILE* stream)
{
    size_t rsize, wsize;

    rsize = ((stream->_bf.size + 1) / 2) & (size_t)~0x03;
    wsize = (stream->_bf.size - rsize) & (size_t)~0x03;

    /* TODO: Base on stream flags. */
    stream->_bf.rbuf.base = stream->_bf.base;
    stream->_bf.rbuf.ptr = stream->_bf.rbuf.base;
    stream->_bf.rbuf.size = rsize;

    stream->_bf.wbuf.base = stream->_bf.base + stream->_bf.rbuf.size;
    stream->_bf.wbuf.ptr = stream->_bf.wbuf.base;
    stream->_bf.wbuf.size = wsize;

    stream->_r = 0;
    stream->_w = stream->_bf.wbuf.size;
}

static int _resize_buf(FILE* stream, size_t size)
{
    _free_buf(stream);

    if (!size)
        return 0;

    stream->_bf.base = malloc(size);

    if (!stream->_bf.base) {
        stream->_r = 0;
        stream->_w = 0;
        return -1;
    }

    stream->_bf.size = size;
    _split_rwbuf(stream);

    return 0;
}

static ssize_t _flush_wbuf(FILE* stream)
{
    size_t write_size, written;

    write_size = stream->_bf.wbuf.size - stream->_w;
    written = _do_system_write(stream->_bf.wbuf.base, write_size, stream);

    if (written != write_size)
        return -EFAULT;

    stream->_w = stream->_bf.wbuf.size;
    stream->_bf.wbuf.ptr = stream->_bf.wbuf.base;
    return (ssize_t)write;
}

/* Stream */

static int _init_stream(FILE* file)
{
    file->_file = -1;
    file->_flags = _IO_MAGIC;
    file->_r = 0;
    file->_w = 0;
    file->_bf.base = NULL;
    file->_bf.size = 0;
    file->_ungotc = UNGOTC_EMPTY;
    return 0;
}

static int _init_file_with_fd(FILE* file, int fd)
{
    _init_stream(file);
    _resize_buf(file, BUFSIZ);
    file->_file = fd;
    return 0;
}

static int _open_file(FILE* file, const char* path, const char* mode)
{
    mode_t flags = 0;
    int err = _parse_mode(mode, &flags);

    if (err)
        return err;

    int fd = open(path, flags);
    if (fd < 0)
        return errno;

    file->_file = fd;
    return 0;
}

static FILE* _fopen_internal(const char* path, const char* mode)
{
    FILE* file = malloc(sizeof(FILE));
    if (!file)
        return NULL;

    _init_stream(file);
    _resize_buf(file, BUFSIZ);
    _open_file(file, path, mode);
    return file;
}

/* Read */

static size_t _do_system_read(char* ptr, size_t size, FILE* stream)
{
    ssize_t read_size = read(stream->_file, ptr, size);
    return read_size < 0 ? 0 : (size_t)read_size;
}

static size_t _do_system_write(const void* ptr, size_t size, FILE* stream)
{
    ssize_t write_size = write(stream->_file, ptr, size);

    if (write_size < 0)
        return 0;

    return (size_t)write_size;
}

static size_t _fread_internal(char* ptr, size_t size, FILE* stream)
{
    size_t total_size, read_from_buf;

    if (!size)
        return 0;

    if (!_can_use_buffer(stream)) {
        total_size = _do_system_read(ptr, size, stream);
        if (total_size < size) {
            stream->_flags |= _IO_EOF_SEEN;
        }
        return total_size;
    }

    total_size = 0;

    /* If the ungot char buffer is not empty, push it onto the buffer first. */
    if (stream->_ungotc != UNGOTC_EMPTY) {
        ptr[0] = (char)stream->_ungotc;
        ptr++;
        size--;
        total_size++;
        stream->_ungotc = UNGOTC_EMPTY;
    }

    /* First read any bytes still sitting in the read buffer. */
    if (stream->_r) {
        read_from_buf = min(stream->_r, size);
        memcpy(ptr, stream->_bf.rbuf.ptr, read_from_buf);
        ptr += read_from_buf;
        size -= read_from_buf;
        stream->_bf.rbuf.ptr += read_from_buf;
        stream->_r -= read_from_buf;
        total_size += read_from_buf;
    }

    /* Read the remaining bytes that were not stored in the read buffer. */
    while (size > 0) {
        stream->_bf.rbuf.ptr = stream->_bf.rbuf.base;
        stream->_r = _do_system_read(
            stream->_bf.rbuf.ptr, stream->_bf.rbuf.size, stream);

        if (!stream->_r) {
            if (size) {
                stream->_flags |= _IO_EOF_SEEN;
            }
            return total_size;
        }

        read_from_buf = min(stream->_r, size);
        memcpy(ptr, stream->_bf.rbuf.ptr, read_from_buf);
        ptr += read_from_buf;
        size -= read_from_buf;
        stream->_bf.rbuf.ptr += read_from_buf;
        stream->_r -= read_from_buf;
        total_size += read_from_buf;
    }

    return total_size;
}

static size_t _fwrite_internal(const void* ptr, size_t size, FILE* stream)
{
    size_t total_size;

    if (!_can_use_buffer(stream))
        return _do_system_write(ptr, size, stream);

    total_size = 0;
    while (size > 0) {
        size_t write_size = min(stream->_w, size);
        memcpy(stream->_bf.wbuf.ptr, ptr, write_size);
        ptr += write_size;
        size -= write_size;
        stream->_bf.wbuf.ptr += write_size;
        stream->_w -= write_size;
        total_size += write_size;

        if (!stream->_w)
            _flush_wbuf(stream);
    }

    return total_size;
}
