
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>

#include "../mecha/mecha.h"

#include "file_reader.h"

#include "idata_reader.h"

struct s_FileReader
{
    IDataReader reader;

    int fd;

    size_t start_offset;
    size_t end_offset;
    char *buffer;
    size_t buffer_len;
};

static bool file_reader_read_exact (IDataReader *inst, char *buf, size_t count);
ssize_t file_reader_read_bytes (IDataReader *inst, char *buf, size_t count);
static bool file_reader_peek_bytes (IDataReader *inst, char *buf, size_t count);
ssize_t file_reader_seek_to_data (IDataReader *instance, const char *needle, size_t count);
static ssize_t file_reader_seek_to_data_func (IDataReader *inst, size_t count, int (*) (const char *ptr));

static bool file_reader_open (IDataReader *inst, const char *file);
static void file_reader_close (IDataReader *inst);

FileReader *
file_reader_new (void)
{
    FileReader *instance = (FileReader *) malloc (sizeof (FileReader));

    instance->reader.open = file_reader_open;
    instance->reader.close = file_reader_close;

    instance->reader.read_exact = file_reader_read_exact;
    instance->reader.read_bytes = file_reader_read_bytes;
    instance->reader.peek_bytes = file_reader_peek_bytes;
    instance->reader.seek_to_data = file_reader_seek_to_data;
    instance->reader.seek_to_data_func = file_reader_seek_to_data_func;

    instance->reader.destroy = file_reader_destroy;

    instance->fd = -1;

    instance->start_offset = 0;
    instance->end_offset = 0;

    instance->buffer_len = 1024;
    instance->buffer = malloc (sizeof (char) * instance->buffer_len);

    return (instance);
}

void
file_reader_destroy (void *ptr)
{
    FileReader *instance = (FileReader *) ptr;

    file_reader_close ((IDataReader *) instance);

    free (instance->buffer);

    free (instance);
}

static bool
file_reader_open (IDataReader *inst, const char *file)
{
    FileReader *instance = (FileReader *) inst;

    instance->fd = open (file, O_RDONLY);

    if (instance->fd == -1)
    {
        return (false);
    }
    else
    {
        instance->start_offset = 0;
        instance->end_offset = 0;

        return (true);
    }
}

static void
file_reader_close (IDataReader *inst)
{
    FileReader *instance = (FileReader *) inst;

    if (instance->fd != -1)
    {
        close (instance->fd);
    }
}

static size_t
file_reader_buffer_len_get (FileReader *instance)
{
    return (instance->end_offset - instance->start_offset);
}

static int
file_reader_buffer_avail_len (FileReader *instance)
{
    return (instance->buffer_len - instance->end_offset);
}

static char *
file_reader_buffer_get (FileReader *instance)
{
    return (instance->buffer + instance->start_offset);
}

static bool
file_reader_buffer_populate (FileReader *instance, size_t minLen)
{
    if (minLen <= file_reader_buffer_len_get (instance))
    {
        return (true);
    }
    else
    {
        int ret;

        if (file_reader_buffer_avail_len (instance) + file_reader_buffer_len_get (instance) < minLen)
        {
            size_t growAmount = 1024;

            if (minLen > growAmount)
            {
                growAmount = minLen;
            }

            /* 
             * if we have more room at the start of the buffer that is unused
             * than we're looking to grow by, we'll just move the buffer to
             * the start.
             */
            if (growAmount < instance->start_offset)
            {
                memmove (instance->buffer, instance->buffer + instance->start_offset, 
                        instance->end_offset - instance->start_offset);

                instance->end_offset = instance->end_offset - instance->start_offset;
                instance->start_offset = 0;
            }
            else
            {
                instance->buffer = realloc (instance->buffer, instance->buffer_len + growAmount);
                instance->buffer_len += growAmount;
            }
        }

        while (file_reader_buffer_len_get (instance) < minLen)
        {
            ret = read (instance->fd, instance->buffer + instance->end_offset, 
                    file_reader_buffer_avail_len (instance));

            if (ret < 1)
            {
                return (false);
            }

            instance->end_offset += ret;
        }

        return (true);
    }
}

static bool
file_reader_buffer_copy (FileReader *instance, char *buf, size_t len)
{
    if (len > file_reader_buffer_len_get (instance))
    {
        return (false);
    }
    else
    {
        memcpy (buf, file_reader_buffer_get (instance), len);

        instance->start_offset += len;

        if (instance->start_offset == instance->end_offset)
        {
            instance->start_offset = 0;
            instance->end_offset = 0;
        }

        return (true);
    }
}

static bool
file_reader_read_exact (IDataReader *inst, char *buf, size_t count)
{
    FileReader *instance = (FileReader *) inst;

    /* 
     * couldn't read the length we needed to, we have to fail -- we want to
     * either read the length we were asked or fail.
     */
    if (file_reader_buffer_populate (instance, count) == false)
    {
        return (false);
    }

    if (file_reader_buffer_copy (instance, buf, count) == true)
    {
        return (true);
    }
    else
    {
        /* 
         * this can't fail -- we already checked that there was enough room to
         * do this copy
         */

        abort();
    }
}

ssize_t
file_reader_read_bytes (IDataReader *inst, char *buf, size_t count)
{
    FileReader *instance = (FileReader *) inst;
    size_t buffer_len = 0;

    /* couldn't read even one byte -- we obviously are at the end */
    if (file_reader_buffer_populate (instance, 1) == false)
    {
        return (-1);
    }

    /* figure out how much we want to read */
    buffer_len = file_reader_buffer_len_get (instance);

    if (count < buffer_len)
    {
        buffer_len = count;
    }

    if (file_reader_buffer_copy (instance, buf, buffer_len) == true)
    {
        return (buffer_len);
    }
    else
    {
        /* 
         * this can't fail -- we already checked that there was enough room to
         * do this copy
         */

        abort();
    }
}

static bool
file_reader_peek_bytes (IDataReader *inst, char *buf, size_t count)
{
    FileReader *instance = (FileReader *) inst;

    /* make sure the buffer has at least the number of bytes we need */
    if (file_reader_buffer_populate (instance, count) == false)
    {
        return (false);
    }

    memcpy (buf, file_reader_buffer_get (instance), count);

    return (true);
}

static ssize_t
file_reader_seek_to_data_func (IDataReader *inst, size_t count, int (*compare_func) (const char *))
{
    FileReader *instance = (FileReader *) inst;
    int skippedBytes = 0;

    assert (count > 0);

    if (count < 1)
    {
        return (-1);
    }

    while (true)
    {
        /* make sure we have at least the number of bytes we're looking for */
        if (file_reader_buffer_populate (instance, count) == false)
        {
            return (-1);
        }

        if (compare_func (file_reader_buffer_get (instance)) == 0)
        {
            /* 
             * if we found it, start_offset is correct, and everything is
             * happy
             */

            return (skippedBytes);
        }
        else
        {
            instance->start_offset++;
            skippedBytes++;
        }
    }
}

ssize_t
file_reader_seek_to_data (IDataReader *inst, const char *needle, size_t count)
{
    FileReader *instance = (FileReader *) inst;
    int skippedBytes = 0;

    assert (count > 0);

    if (count < 1)
    {
        return (-1);
    }

    while (true)
    {
        char *ptr;

        /* make sure we have at least the number of bytes we're looking for */
        if (file_reader_buffer_populate (instance, count) == false)
        {
            return (-1);
        }

        ptr = memchr (file_reader_buffer_get (instance), needle[0], file_reader_buffer_len_get (instance));

        if (ptr != NULL)
        {
            int new_start_offset = ptr - instance->buffer;

            skippedBytes += new_start_offset - instance->start_offset;

            instance->start_offset = new_start_offset;

            /* 
             * if we don't have enough bytes in the buffer, we'll just do this
             * again (which will force enough bytes on the buffer)
             */
            if (file_reader_buffer_len_get (instance) < count)
            {
                continue;
            }

            if (memcmp (ptr, needle, count) == 0)
            {
                /* 
                 * if we found it, start_offset is correct, and everything is
                 * happy
                 */

                return (skippedBytes);
            }
            else
            {
                /* 
                 * if we didn't find it, so we'll skip one more byte so that
                 * the next time around, we'll be searching the rest of the
                 * data.
                 */

                skippedBytes++;
                instance->start_offset++;

                continue;
            }
        }
        else
        {
            /* 
             * if we didn't find the first byte anywhere.. we'll have to ditch
             * this buffer and try for another one
             */
            skippedBytes += instance->end_offset - instance->start_offset;

            instance->start_offset = 0;
            instance->end_offset = 0;
        }
    }
}

