/* minideflate.c -- test deflate/inflate under specific conditions
 * Copyright (C) 2020 Nathan Moinvaziri
 * For conditions of distribution and use, see copyright notice in zlib.h
 */

#define _POSIX_SOURCE 1  /* This file needs POSIX for fileno(). */
#define _POSIX_C_SOURCE 200112  /* For snprintf(). */

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <inttypes.h>

#include "zbuild.h"
#include "zutil.h"

#if defined(_WIN32) || defined(__CYGWIN__)
#  include <fcntl.h>
#  include <io.h>
#  include <string.h>
#  define SET_BINARY_MODE(file) setmode(fileno(file), O_BINARY)
#  ifdef _MSC_VER
#    define strcasecmp _stricmp
#  endif
#else
#  include <strings.h>
#  define SET_BINARY_MODE(file)
#endif

#define CHECK_ERR(err, msg) { \
    if (err != Z_OK) { \
        fprintf(stderr, "%s error: %d\n", msg, err); \
        exit(1); \
    } \
}

/* Default read/write i/o buffer size based on GZBUFSIZE */
#define BUFSIZE 131072

/* ===========================================================================
 * deflate() using specialized parameters
 */
void deflate_params(FILE *fin, FILE *fout, int32_t read_buf_size, int32_t write_buf_size, int32_t level,
    int32_t window_bits, int32_t mem_level, int32_t strategy, int32_t flush) {
    PREFIX3(stream) c_stream; /* compression stream */
    uint8_t *read_buf;
    uint8_t *write_buf;
    int32_t read;
    int err;

    read_buf = (uint8_t *)malloc(read_buf_size);
    if (read_buf == NULL) {
        fprintf(stderr, "failed to create read buffer (%d)\n", read_buf_size);
        return;
    }
    write_buf = (uint8_t *)malloc(write_buf_size);
    if (write_buf == NULL) {
        fprintf(stderr, "failed to create write buffer (%d)\n", write_buf_size);
        free(read_buf);
        return;
    }

    c_stream.zalloc = NULL;
    c_stream.zfree = NULL;
    c_stream.opaque = (void *)0;
    c_stream.total_in = 0;
    c_stream.total_out = 0;
    c_stream.next_out = write_buf;
    c_stream.avail_out = write_buf_size;

    err = PREFIX(deflateInit2)(&c_stream, level, Z_DEFLATED, window_bits, mem_level, strategy);
    CHECK_ERR(err, "deflateInit2");

    /* Process input using our read buffer and flush type,
     * output to stdout only once write buffer is full */
    do {
        read = (int32_t)fread(read_buf, 1, read_buf_size, fin);
        if (read <= 0)
            break;

        c_stream.next_in  = (z_const uint8_t *)read_buf;
        c_stream.avail_in = read;

        do {
            err = PREFIX(deflate)(&c_stream, flush);
            if (err == Z_STREAM_END) break;
            CHECK_ERR(err, "deflate");

            if (c_stream.next_out == write_buf + write_buf_size) {
                fwrite(write_buf, 1, write_buf_size, fout);
                c_stream.next_out = write_buf;
                c_stream.avail_out = write_buf_size;
            }
        } while (c_stream.next_in < read_buf + read);
    } while (err == Z_OK);

    /* Finish the stream if necessary */
    if (flush != Z_FINISH) {
        c_stream.avail_in = 0;
        do {
            if (c_stream.next_out == write_buf + write_buf_size) {
                fwrite(write_buf, 1, write_buf_size, fout);
                c_stream.next_out = write_buf;
                c_stream.avail_out = write_buf_size;
            }

            err = PREFIX(deflate)(&c_stream, Z_FINISH);
            if (err == Z_STREAM_END) break;
            CHECK_ERR(err, "deflate");
        } while (err == Z_OK);
    }

    /* Output remaining data in write buffer */
    if (c_stream.next_out != write_buf) {
        fwrite(write_buf, 1, c_stream.next_out - write_buf, fout);
    }

    err = PREFIX(deflateEnd)(&c_stream);
    CHECK_ERR(err, "deflateEnd");

    free(read_buf);
    free(write_buf);
}

/* ===========================================================================
 * inflate() using specialized parameters
 */
void inflate_params(FILE *fin, FILE *fout, int32_t read_buf_size, int32_t write_buf_size, int32_t window_bits,
    int32_t flush) {
    PREFIX3(stream) d_stream; /* decompression stream */
    uint8_t *read_buf;
    uint8_t *write_buf;
    int32_t read;
    int err;


    read_buf = (uint8_t *)malloc(read_buf_size);
    if (read_buf == NULL) {
        fprintf(stderr, "failed to create read buffer (%d)\n", read_buf_size);
        return;
    }
    write_buf = (uint8_t *)malloc(write_buf_size);
    if (write_buf == NULL) {
        fprintf(stderr, "failed to create write buffer (%d)\n", write_buf_size);
        free(read_buf);
        return;
    }

    d_stream.zalloc = NULL;
    d_stream.zfree = NULL;
    d_stream.opaque = (void *)0;
    d_stream.total_in = 0;
    d_stream.total_out = 0;
    d_stream.next_out = write_buf;
    d_stream.avail_out = write_buf_size;

    err = PREFIX(inflateInit2)(&d_stream, window_bits);
    CHECK_ERR(err, "inflateInit2");

    /* Process input using our read buffer and flush type,
     * output to stdout only once write buffer is full */
    do {
        read = (int32_t)fread(read_buf, 1, read_buf_size, fin);
        if (read <= 0)
            break;

        d_stream.next_in  = (z_const uint8_t *)read_buf;
        d_stream.avail_in = read;

        do {
            err = PREFIX(inflate)(&d_stream, flush);
            if (err == Z_STREAM_END) break;
            CHECK_ERR(err, "inflate");

            if (d_stream.next_out == write_buf + write_buf_size) {
                fwrite(write_buf, 1, write_buf_size, fout);
                d_stream.next_out = write_buf;
                d_stream.avail_out = write_buf_size;
            }
        } while (d_stream.next_in < read_buf + read);
    } while (err == Z_OK);

    /* Finish the stream if necessary */
    if (flush != Z_FINISH) {
        d_stream.avail_in = 0;
        do {
            if (d_stream.next_out == write_buf + write_buf_size) {
                fwrite(write_buf, 1, write_buf_size, fout);
                d_stream.next_out = write_buf;
                d_stream.avail_out = write_buf_size;
            }

            err = PREFIX(inflate)(&d_stream, Z_FINISH);
            if (err == Z_STREAM_END) break;
            CHECK_ERR(err, "inflate");
        } while (err == Z_OK);
    }

    /* Output remaining data in write buffer */
    if (d_stream.next_out != write_buf) {
        fwrite(write_buf, 1, d_stream.next_out - write_buf, fout);
    }

    err = PREFIX(inflateEnd)(&d_stream);
    CHECK_ERR(err, "inflateEnd");

    free(read_buf);
    free(write_buf);
}

void show_help(void) {
    printf("Usage: minideflate [-c][-d][-k] [-f|-h|-R|-F] [-m level] [-r/-t size] [-s flush] [-w bits] [-0 to -9] [input file]\n\n" \
           "  -c : write to standard output\n" \
           "  -d : decompress\n" \
           "  -k : keep input file\n" \
           "  -f : compress with Z_FILTERED\n" \
           "  -h : compress with Z_HUFFMAN_ONLY\n" \
           "  -R : compress with Z_RLE\n" \
           "  -F : compress with Z_FIXED\n" \
           "  -m : memory level (1 to 8)\n" \
           "  -w : window bits..\n" \
           "     :   -1 to -15 for raw deflate\n"
           "     :    0 to  15 for deflate (adler32)\n"
           "     :   16 to  31 for gzip (crc32)\n"
           "  -s : flush type (0 to 5)\n" \
           "  -r : read buffer size\n" \
           "  -t : write buffer size\n" \
           "  -0 to -9 : compression level\n\n");
}

int main(int argc, char **argv) {
    int32_t i;
    int32_t mem_level = DEF_MEM_LEVEL;
    int32_t window_bits = INT32_MAX;
    int32_t strategy = Z_DEFAULT_STRATEGY;
    int32_t level = Z_DEFAULT_COMPRESSION;
    int32_t read_buf_size = BUFSIZE;
    int32_t write_buf_size = BUFSIZE;
    int32_t flush = Z_NO_FLUSH;
    uint8_t copyout = 0;
    uint8_t uncompr = 0;
    uint8_t keep = 0;
    FILE *fin = stdin;
    FILE *fout = stdout;


    if (argc == 1) {
        show_help();
        return 64;   /* EX_USAGE */
    }

    for (i = 1; i < argc; i++) {
        if ((strcmp(argv[i], "-m") == 0) && (i + 1 < argc))
            mem_level = atoi(argv[++i]);
        else if ((strcmp(argv[i], "-w") == 0) && (i + 1 < argc))
            window_bits = atoi(argv[++i]);
        else if ((strcmp(argv[i], "-r") == 0) && (i + 1 < argc))
            read_buf_size = atoi(argv[++i]);
        else if ((strcmp(argv[i], "-t") == 0) && (i + 1 < argc))
            write_buf_size = atoi(argv[++i]);
        else if ((strcmp(argv[i], "-s") == 0) && (i + 1 < argc))
            flush = atoi(argv[++i]);
        else if (strcmp(argv[i], "-c") == 0)
            copyout = 1;
        else if (strcmp(argv[i], "-d") == 0)
            uncompr = 1;
        else if (strcmp(argv[i], "-k") == 0)
            keep = 1;
        else if (strcmp(argv[i], "-f") == 0)
            strategy = Z_FILTERED;
        else if (strcmp(argv[i], "-F") == 0)
            strategy = Z_FIXED;
        else if (strcmp(argv[i], "-h") == 0)
            strategy = Z_HUFFMAN_ONLY;
        else if (strcmp(argv[i], "-R") == 0)
            strategy = Z_RLE;
        else if (argv[i][0] == '-' && argv[i][1] >= '0' && argv[i][1] <= '9' && argv[i][2] == 0)
            level = argv[i][1] - '0';
        else if (strcmp(argv[i], "--help") == 0) {
            show_help();
            return 0;
        } else if (argv[i][0] == '-') {
            show_help();
            return 64;   /* EX_USAGE */
        } else 
            break;
    }

    SET_BINARY_MODE(stdin);
    SET_BINARY_MODE(stdout);

    if (i != argc) {
        fin = fopen(argv[i], "rb+");
        if (fin == NULL) {
            fprintf(stderr, "Failed to open file: %s\n", argv[i]);
            exit(1);
        }
        if (!copyout) {
            char *out_file = (char *)calloc(1, strlen(argv[i]) + 6);
            if (out_file == NULL) {
                fprintf(stderr, "Not enough memory\n");
                exit(1);
            }
            strcat(out_file, argv[i]);
            if (!uncompr) {
                if (window_bits < 0) {
                    strcat(out_file, ".zraw");
                } else if (window_bits > MAX_WBITS) {
                    strcat(out_file, ".gz");
                } else {
                    strcat(out_file, ".z");
                }
            } else {
                char *out_ext = strrchr(out_file, '.');
                if (out_ext != NULL) {
                    if (strcasecmp(out_ext, ".zraw") == 0 && window_bits == INT32_MAX) {
                        fprintf(stderr, "Must specify window bits for raw deflate stream\n");
                        exit(1);
                    }
                    *out_ext = 0;
                }
            }
            fout = fopen(out_file, "wb");
            if (fout == NULL) {
                fprintf(stderr, "Failed to open file: %s\n", out_file);
                exit(1);
            }
            free(out_file);
        }
    }
    
    if (window_bits == INT32_MAX) {
        window_bits = MAX_WBITS;
        /* Auto-detect wrapper for inflateInit */
        if (uncompr)
            window_bits += 32;
    }

    if (uncompr) {
        inflate_params(fin, fout, read_buf_size, write_buf_size, window_bits, flush);
    } else {
        deflate_params(fin, fout, read_buf_size, write_buf_size, level, window_bits, mem_level, strategy, flush);
    }

    if (fin != stdin) {
        fclose(fin);
        if (!copyout && !keep) {
            unlink(argv[i]);
        }
    }
    if (fout != stdout) {
        fclose(fout);
    }

    return 0;
}
