/*
 *  libcaca     Colour ASCII-Art library
 *  Copyright © 2006—2018 Sam Hocevar <sam@hocevar.net>
 *              All Rights Reserved
 *
 *  This library is free software. It comes without any warranty, to
 *  the extent permitted by applicable law. You can redistribute it
 *  and/or modify it under the terms of the Do What the Fuck You Want
 *  to Public License, Version 2, as published by Sam Hocevar. See
 *  http://www.wtfpl.net/ for more details.
 */

/*
 *  This file contains functions for compressed file I/O.
 */

#include "config.h"

#if !defined __KERNEL__
#   include <stdio.h>
#   include <stdlib.h>
#   include <string.h>
#   if defined HAVE_ZLIB_H
#       include <zlib.h>
#       define READSIZE  128 /* Read buffer size */
#       define WRITESIZE 128 /* Inflate buffer size */
#   endif
#endif

#include "caca.h"
#include "caca_internals.h"

#if !defined __KERNEL__ && defined HAVE_ZLIB_H
static int zipread(caca_file_t *, void *, unsigned int);
#endif

#if !defined __KERNEL__
struct caca_file
{
#   if defined HAVE_ZLIB_H
    uint8_t read_buffer[READSIZE];
    z_stream stream;
    gzFile gz;
    int eof, zip, total;
#   endif
    FILE *f;
    int readonly;
};
#endif

/** \brief Open a file for reading or writing
 *
 *  Create a caca file handle for a file. If the file is zipped, it is
 *  decompressed on the fly.
 *
 *  If an error occurs, NULL is returned and \b errno is set accordingly:
 *  - \c ENOSTS Function not implemented.
 *  - \c EINVAL File not found or permission denied.
 *
 *  \param path The file path
 *  \param mode The file open mode
 *  \return A file handle to \e path.
 */
caca_file_t *caca_file_open(char const *path, const char *mode)
{
#if defined __KERNEL__
    seterrno(ENOSYS);
    return NULL;
#else
    caca_file_t *fp = malloc(sizeof(*fp));

    fp->readonly = !!strchr(mode, 'r');

#   if defined HAVE_ZLIB_H
    uint8_t buf[4];
    unsigned int skip_size = 0;

    fp->gz = gzopen(path, fp->readonly ? "rb" : "wb");
    if(!fp->gz)
    {
        free(fp);
        seterrno(EINVAL);
        return NULL;
    }

    fp->eof = 0;
    fp->zip = 0;
    fp->total = 0;

    if(fp->readonly)
    {
        /* Parse ZIP file and go to start of first file */
        gzread(fp->gz, buf, 4);
        if(memcmp(buf, "PK\3\4", 4))
        {
            gzseek(fp->gz, 0, SEEK_SET);
            return fp;
        }

        fp->zip = 1;

        gzseek(fp->gz, 22, SEEK_CUR);

        gzread(fp->gz, buf, 2); /* Filename size */
        skip_size += (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
        gzread(fp->gz, buf, 2); /* Extra field size */
        skip_size += (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);

        gzseek(fp->gz, skip_size, SEEK_CUR);

        /* Initialise inflate stream */
        fp->stream.total_out = 0;
        fp->stream.zalloc = NULL;
        fp->stream.zfree = NULL;
        fp->stream.opaque = NULL;
        fp->stream.next_in = NULL;
        fp->stream.avail_in = 0;

        if(inflateInit2(&fp->stream, -MAX_WBITS))
        {
            gzclose(fp->gz);
            free(fp);
            seterrno(EINVAL);
            return NULL;
        }
    }
#   else
    fp->f = fopen(path, mode);

    if(!fp->f)
    {
        free(fp);
        seterrno(EINVAL);
        return NULL;
    }
#   endif

    return fp;
#endif
}

/** \brief Close a file handle
 *
 *  Close and destroy the resources associated with a caca file handle.
 *
 *  This function is a wrapper for fclose() or, if available, gzclose().
 *
 *  \param fp The file handle
 *  \return The return value of fclose() or gzclose().
 */
int caca_file_close(caca_file_t *fp)
{
#if defined __KERNEL__
    seterrno(ENOSYS);
    return 0;
#elif defined HAVE_ZLIB_H
    gzFile gz = fp->gz;
    if(fp->zip)
        inflateEnd(&fp->stream);
    free(fp);
    return gzclose(gz);
#else
    FILE *f = fp->f;
    free(fp);
    return fclose(f);
#endif
}

/** \brief Return the position in a file handle
 *
 *  Return the file handle position, in bytes.
 *
 *  \param fp The file handle
 *  \return The current offset in the file handle.
 */
uint64_t caca_file_tell(caca_file_t *fp)
{
#if defined __KERNEL__
    seterrno(ENOSYS);
    return 0;
#elif defined HAVE_ZLIB_H
    if(fp->zip)
        return fp->total;
    return gztell(fp->gz);
#else
    return ftell(fp->f);
#endif
}

/** \brief Read data from a file handle
 *
 *  Read data from a file handle and copy them into the given buffer.
 *
 *  \param fp The file handle
 *  \param ptr The destination buffer
 *  \param size The number of bytes to read
 *  \return The number of bytes read
 */
size_t caca_file_read(caca_file_t *fp, void *ptr, size_t size)
{
#if defined __KERNEL__
    seterrno(ENOSYS);
    return 0;
#elif defined HAVE_ZLIB_H
    if(fp->zip)
        return zipread(fp, ptr, size);
    return gzread(fp->gz, ptr, size);
#else
    return fread(ptr, 1, size, fp->f);
#endif
}

/** \brief Write data to a file handle
 *
 *  Write the contents of the given buffer to the file handle.
 *
 *  \param fp The file handle
 *  \param ptr The source buffer
 *  \param size The number of bytes to write
 *  \return The number of bytes written
 */
size_t caca_file_write(caca_file_t *fp, const void *ptr, size_t size)
{
#if defined __KERNEL__
    seterrno(ENOSYS);
    return 0;
#else
    if(fp->readonly)
        return 0;

#   if defined HAVE_ZLIB_H
    if(fp->zip)
    {
        /* FIXME: zip files are not supported */
        seterrno(ENOSYS);
        return 0;
    }
    return gzwrite(fp->gz, ptr, size);
#   else
    return fwrite(ptr, 1, size, fp->f);
#   endif
#endif
}

/** \brief Read a line from a file handle
 *
 *  Read one line of data from a file handle, up to one less than the given
 *  number of bytes. A trailing zero is appended to the data.
 *
 *  \param fp The file handle
 *  \param s The destination buffer
 *  \param size The maximum number of bytes to read
 *  \return The number of bytes read, including the trailing zero
 */
char *caca_file_gets(caca_file_t *fp, char *s, int size)
{
#if defined __KERNEL__
    seterrno(ENOSYS);
    return NULL;
#elif defined HAVE_ZLIB_H
    if(fp->zip)
    {
        int i;

        for(i = 0; i < size; i++)
        {
            int ret = zipread(fp, s + i, 1);

            if(ret < 0)
                return NULL;

            if(ret == 0 || s[i] == '\n')
            {
                if(i + 1 < size)
                    s[i + 1] = '\0';
                return s;
            }
        }

        return s;
    }

    return gzgets(fp->gz, s, size);
#else
    return fgets(s, size, fp->f);
#endif
}

/** \brief Tell whether a file handle reached end of file
 *
 *  Return the end-of-file status of the file handle.
 *
 *  This function is a wrapper for feof() or, if available, gzeof().
 *
 *  \param fp The file handle
 *  \return 1 if EOF was reached, 0 otherwise
 */
int caca_file_eof(caca_file_t *fp)
{
#if defined __KERNEL__
    return 1;
#elif defined HAVE_ZLIB_H
    return fp->zip ? fp->eof : gzeof(fp->gz);
#else
    return feof(fp->f);
#endif
}

#if !defined __KERNEL__ && defined HAVE_ZLIB_H
static int zipread(caca_file_t *fp, void *buf, unsigned int len)
{
    unsigned int total_read = 0;

    if(len == 0)
        return 0;

    fp->stream.next_out = buf;
    fp->stream.avail_out = len;

    while(fp->stream.avail_out > 0)
    {
        unsigned int tmp;
        int ret = 0;

        if(fp->stream.avail_in == 0 && !gzeof(fp->gz))
        {
            int bytes_read;

            bytes_read = gzread(fp->gz, fp->read_buffer, READSIZE);
            if(bytes_read < 0)
                return -1;

            fp->stream.next_in = fp->read_buffer;
            fp->stream.avail_in = bytes_read;
        }

        tmp = fp->stream.total_out;
        ret = inflate(&fp->stream, Z_SYNC_FLUSH);
        total_read += fp->stream.total_out - tmp;

        if(ret == Z_STREAM_END)
        {
            fp->eof = 1;
            fp->total += total_read;
            return total_read;
        }

        if(ret != Z_OK)
            return ret;
    }

    fp->total += total_read;
    return total_read;
}
#endif