/**************************************************************************
 * nkitool - Console application which exports and imports the human      *
 *           readable XML file from and to Kontakt .nki instrument        *
 *           articulation files (Kontakt formats v1 to v4 are supported). *
 *                                                                        *
 * Version 1.0 (2010-08-07) Christian Schoenebeck <cuse@users.sf.net>     *
 *                                                                        *
 * You can get the latest version and notes at:                           *
 * http://www.linuxsampler.org/nkitool/                                   *
 *                                                                        *
 * The LinuxSampler Project -- http://www.linuxsampler.org                *
 *                                                                        *
 * Not copyrighted -- provided to the public domain. In case you find     *
 * mistakes or improvements, we would appreciate if you share them with   *
 * us!                                                                    *
 **************************************************************************/

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include "zlib-1.2.5/zlib.h"

#if defined(MSDOS) || defined(OS2) || defined(WIN32) || defined(__CYGWIN__)
#  include <fcntl.h>
#  include <io.h>
#  define SET_BINARY_MODE(file) setmode(fileno(file), O_BINARY)
#else
#  define SET_BINARY_MODE(file)
#endif

#define CHUNK 16384

/* Compress from file source to file dest until EOF on source.
   def() returns Z_OK on success, Z_MEM_ERROR if memory could not be
   allocated for processing, Z_STREAM_ERROR if an invalid compression
   level is supplied, Z_VERSION_ERROR if the version of zlib.h and the
   version of the library linked do not match, or Z_ERRNO if there is
   an error reading or writing the files. */
int def(FILE *source, FILE *dest, int level)
{
    int ret, flush;
    unsigned have;
    z_stream strm;
    unsigned char in[CHUNK];
    unsigned char out[CHUNK];

    /* allocate deflate state */
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    ret = deflateInit(&strm, level);
    if (ret != Z_OK)
        return ret;

    /* compress until end of file */
    do {
        strm.avail_in = fread(in, 1, CHUNK, source);
        if (ferror(source)) {
            (void)deflateEnd(&strm);
            return Z_ERRNO;
        }
        flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;
        strm.next_in = in;

        /* run deflate() on input until output buffer not full, finish
           compression if all of source has been read in */
        do {
            strm.avail_out = CHUNK;
            strm.next_out = out;
            ret = deflate(&strm, flush);    /* no bad return value */
            assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
            have = CHUNK - strm.avail_out;
            if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
                (void)deflateEnd(&strm);
                return Z_ERRNO;
            }
        } while (strm.avail_out == 0);
        assert(strm.avail_in == 0);     /* all input will be used */

        /* done when last data in file processed */
    } while (flush != Z_FINISH);
    assert(ret == Z_STREAM_END);        /* stream will be complete */

    /* clean up and return */
    (void)deflateEnd(&strm);
    return Z_OK;
}

/* Decompress from file source to file dest until stream ends or EOF.
   inf() returns Z_OK on success, Z_MEM_ERROR if memory could not be
   allocated for processing, Z_DATA_ERROR if the deflate data is
   invalid or incomplete, Z_VERSION_ERROR if the version of zlib.h and
   the version of the library linked do not match, or Z_ERRNO if there
   is an error reading or writing the files. */
int inf(FILE *source, FILE *dest)
{
    int ret;
    unsigned have;
    z_stream strm;
    unsigned char in[CHUNK];
    unsigned char out[CHUNK];

    /* allocate inflate state */
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    strm.avail_in = 0;
    strm.next_in = Z_NULL;
    ret = inflateInit(&strm);
    if (ret != Z_OK)
        return ret;

    /* decompress until deflate stream ends or end of file */
    do {
        strm.avail_in = fread(in, 1, CHUNK, source);
        if (ferror(source)) {
            (void)inflateEnd(&strm);
            return Z_ERRNO;
        }
        if (strm.avail_in == 0)
            break;
        strm.next_in = in;

        /* run inflate() on input until output buffer not full */
        do {
            strm.avail_out = CHUNK;
            strm.next_out = out;
            ret = inflate(&strm, Z_NO_FLUSH);
            assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
            switch (ret) {
            case Z_NEED_DICT:
                ret = Z_DATA_ERROR;     /* and fall through */
            case Z_DATA_ERROR:
            case Z_MEM_ERROR:
                (void)inflateEnd(&strm);
                return ret;
            }
            have = CHUNK - strm.avail_out;
            if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
                (void)inflateEnd(&strm);
                return Z_ERRNO;
            }
        } while (strm.avail_out == 0);

        /* done when inflate() says it's done */
    } while (ret != Z_STREAM_END);

    /* clean up and return */
    (void)inflateEnd(&strm);
    return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR;
}

/* report a zlib or i/o error */
void zerr(int ret)
{
    fputs("zpipe: ", stderr);
    switch (ret) {
    case Z_ERRNO:
        if (ferror(stdin))
            fputs("error reading stdin\n", stderr);
        if (ferror(stdout))
            fputs("error writing stdout\n", stderr);
        break;
    case Z_STREAM_ERROR:
        fputs("invalid compression level\n", stderr);
        break;
    case Z_DATA_ERROR:
        fputs("invalid or incomplete deflate data\n", stderr);
        break;
    case Z_MEM_ERROR:
        fputs("out of memory\n", stderr);
        break;
    case Z_VERSION_ERROR:
        fputs("zlib version mismatch!\n", stderr);
    }
}

const unsigned char nki4Footer[229] =
{
    0xae, 0xe1, 0x0e, 0xb0,
    0x01, 0x01, 0x0c, 0x00,
    0xd9, 0x00, 0x00, 0x00,
    0x3c, 0x3f, 0x78, 0x6d,
    0x6c, 0x20, 0x76, 0x65,
    0x72, 0x73, 0x69, 0x6f,
    0x6e, 0x3d, 0x22, 0x31,
    0x2e, 0x30, 0x22, 0x20,
    0x65, 0x6e, 0x63, 0x6f,
    0x64, 0x69, 0x6e, 0x67,
    0x3d, 0x22, 0x55, 0x54,
    0x46, 0x2d, 0x38, 0x22,
    0x20, 0x73, 0x74, 0x61,
    0x6e, 0x64, 0x61, 0x6c,
    0x6f, 0x6e, 0x65, 0x3d,
    0x22, 0x6e, 0x6f, 0x22,
    0x20, 0x3f, 0x3e, 0x0a,
    0x3c, 0x73, 0x6f, 0x75,
    0x6e, 0x64, 0x69, 0x6e,
    0x66, 0x6f, 0x20, 0x76,
    0x65, 0x72, 0x73, 0x69,
    0x6f, 0x6e, 0x3d, 0x22,
    0x34, 0x30, 0x30, 0x22,
    0x3e, 0x0a, 0x0a, 0x20,
    0x20, 0x3c, 0x70, 0x72,
    0x6f, 0x70, 0x65, 0x72,
    0x74, 0x69, 0x65, 0x73,
    0x2f, 0x3e, 0x0a, 0x0a,
    0x20, 0x20, 0x3c, 0x61,
    0x74, 0x74, 0x72, 0x69,
    0x62, 0x75, 0x74, 0x65,
    0x73, 0x3e, 0x0a, 0x20,
    0x20, 0x20, 0x20, 0x3c,
    0x61, 0x74, 0x74, 0x72,
    0x69, 0x62, 0x75, 0x74,
    0x65, 0x3e, 0x0a, 0x20,
    0x20, 0x20, 0x20, 0x20,
    0x20, 0x3c, 0x76, 0x61,
    0x6c, 0x75, 0x65, 0x3e,
    0x4b, 0x6f, 0x6e, 0x74,
    0x61, 0x6b, 0x74, 0x49,
    0x6e, 0x73, 0x74, 0x72,
    0x75, 0x6d, 0x65, 0x6e,
    0x74, 0x3c, 0x2f, 0x76,
    0x61, 0x6c, 0x75, 0x65,
    0x3e, 0x0a, 0x20, 0x20,
    0x20, 0x20, 0x3c, 0x2f,
    0x61, 0x74, 0x74, 0x72,
    0x69, 0x62, 0x75, 0x74,
    0x65, 0x3e, 0x0a, 0x20,
    0x20, 0x3c, 0x2f, 0x61,
    0x74, 0x74, 0x72, 0x69,
    0x62, 0x75, 0x74, 0x65,
    0x73, 0x3e, 0x0a, 0x0a,
    0x3c, 0x2f, 0x73, 0x6f,
    0x75, 0x6e, 0x64, 0x69,
    0x6e, 0x66, 0x6f, 0x3e,
    0x0a
};

int main(int argc, char **argv)
{
    FILE* input;
    FILE* output;
    int ret;
    ret = 0;

    /* avoid end-of-line conversions */
    SET_BINARY_MODE(stdin);
    SET_BINARY_MODE(stdout);

    if (argc == 2) { // extract
        char xmlName[128];
        snprintf(xmlName, 128, "%s.xml", argv[1]);
        printf("Extracting '%s' from '%s' ...\n", xmlName, argv[1]);
        
        input = fopen(argv[1], "rb");
        if (!input) {
            fprintf(stderr, "Could not open input file '%s'\n", argv[1]);
            return 1;
        }
        
        output = fopen(xmlName, "wb");
        if (!output) {
            fprintf(stderr, "Could not open output file '%s'\n", xmlName);
            fclose(input);
            return 1;
        }
        
        // skip 170 bytes
        fseek(input, 170, SEEK_SET);
        
        ret = inf(input, output);
        if (ret != Z_OK) zerr(ret);

        fclose(input);
        fclose(output);
    } else if (argc == 3) { // replace
        char nkiHeader[172];
        printf("Replacing '%s' with '%s' ...\n", argv[1], argv[2]);
        
        // first read the 170 byte header of the existing .nki file
        input = fopen(argv[1], "rb");
        if (!input) {
            fprintf(stderr, "Could not open '%s' for reading its NKI header\n", argv[1]);
            return 1;
        }
        ret = fread(nkiHeader, 1, 172, input);
        if (ret < 172) {
            fprintf(stderr, "Could not read NKI header from '%s'\n", argv[1]);
            fclose(input);
            return 1;
        }
        // check if destination .nki got Kontakt 4 footer
        fseek(input, -13, SEEK_END);
        const char nki4EndTag[] = "</soundinfo>";
        char nki4TestBuf[12];
        fread(nki4TestBuf, 1, 12, input);
        const int bIsNki4 = (strncmp(nki4EndTag, nki4TestBuf, 12) == 0);
        fclose(input);
        
        // now we can truncate the nki file to 0 byte length and write the new file

        input = fopen(argv[2], "rb");
        if (!input) {
            fprintf(stderr, "Could not open input XML file '%s'\n", argv[2]);
            return 1;
        }
        
        output = fopen(argv[1], "wb");
        if (!output) {
            fprintf(stderr, "Could not open output NKI file '%s'\n", argv[1]);
            fclose(input);
            return 1;
        }
        
        // restore the NKI header
        fwrite(nkiHeader, 1, 170, output);

        const int compressionLevel = (nkiHeader[171] == 1) ? 1 : Z_DEFAULT_COMPRESSION;
        if (compressionLevel == Z_DEFAULT_COMPRESSION) {
            printf("Using Kontakt v1, v2 compression format.\n");
        } else {
            printf("Using Kontakt v3+ compression format.\n");
        }
        
        ret = def(input, output, compressionLevel);
        if (ret != Z_OK) zerr(ret);
        
        // if target is NKI v4 then append the required footer
        if (bIsNki4) {
            printf("Appending Kontakt v4 footer.\n");
            fwrite(nki4Footer, 1, 229, output);
        }
        
        fclose(input);
        fclose(output);
    } else {
        fputs("Exports and imports the human readable XML file from and to Kontakt\n", stderr);
        fputs(".nki instrument articulation files (Kontakt format v1 ... v4).\n\n", stderr);
        fputs("nki.exe usage:\n\n", stderr);
        fputs("Extract XML file from some .nki file:\n", stderr);
        fputs("\tnki.exe Foo.nki\n\n", stderr);
        fputs("Replace .nki file with another XML file:\n", stderr);
        fputs("\tnki.exe Foo.nki Foo.xml\n\n", stderr);
        return 1;
    }

    return ret;
}
