/*
 * apck_pack.h - Asset Pack (packer)
 * https://gitlab.com/bztsrc/assetpack
 *
 * Copyright (C) 2024 bzt, MIT license
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL ANY
 * DEVELOPER OR DISTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @brief Asset Pack archive creator single header library
 */

#ifndef APCK_PACK_H
#define APCK_PACK_H
#ifdef  __cplusplus
extern "C" {
#endif

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>
#include <apck.h>

typedef uint64_t (*apck_writecb_t)(void *enc, void *f, uint64_t offs, const uint8_t *buf, uint64_t size);
typedef struct {
    apck_hdr_t hdr;
    void *f;
    void *enc;
    uint32_t strslen, strsnum, *strspos;
    char *strs;
    apck_writecb_t writecb;
    uint32_t pad;
    uint32_t numfiles[APCK_NUMTYPES];
    apck_file_t *files[APCK_NUMTYPES];
    uint64_t offs;
} apck_pack_t;

uint8_t *apck_pack_uleb128(uint8_t *ptr, uint64_t value);
int apck_pack_init(apck_pack_t *ctx, void *f, char *engine, uint32_t version);
int apck_pack_encrypt(apck_pack_t *ctx, uint32_t pad, uint8_t *enckey, apck_initcb_t initcb, apck_writecb_t writecb);
int apck_pack_write(apck_pack_t *ctx, uint64_t offs, uint8_t *buf, uint64_t size);
int apck_pack_flush(apck_pack_t *ctx);
uint32_t apck_pack_str(apck_pack_t *ctx, char *name);
int apck_pack_add(apck_pack_t *ctx, int type, char *name, uint8_t *buf, uint64_t size);
int apck_pack_asset(apck_pack_t *ctx, int type, char *name, void *buf, uint64_t size, int w, int h);
uint8_t *apck_map_encode(apck_pack_t *ctx, apck_map_t *map, uint64_t *size);

#ifdef APCK_PACK_IMPLEMENTATION

/**
 * Write out an ULEB128 number
 */
uint8_t *apck_pack_uleb128(uint8_t *ptr, uint64_t value)
{
    uint8_t b;
    do {
        b = value & 0x7F;
        value >>= 7;
        *ptr++ = b | (value ? 0x80 : 0);
    } while(value);
    return ptr;
}

/**
 * Initialize the packer context
 */
int apck_pack_init(apck_pack_t *ctx, void *f, char *engine, uint32_t version)
{
    if(!ctx || !f) return APCK_ERR_BADINP;

    memset(ctx, 0, sizeof(apck_pack_t));
    memcpy(ctx->hdr.magic, APCK_MAGIC, 4);
    if(engine) strncpy((char*)ctx->hdr.engine, engine, 12);
    ctx->hdr.version = version;
    ctx->f = f;
    return APCK_OK;
}

/**
 * Set encryption for archive
 */
int apck_pack_encrypt(apck_pack_t *ctx, uint32_t pad, uint8_t *enckey, apck_initcb_t initcb, apck_writecb_t writecb)
{
    if(!ctx || pad > 512 || !enckey || !initcb || !writecb) return APCK_ERR_BADINP;

    memcpy(ctx->hdr.enckey, enckey, 32);
    ctx->enc = (*initcb)(enckey);
    ctx->writecb = writecb;
    ctx->pad = pad;
    return APCK_OK;
}

/**
 * Function to write to archive
 */
int apck_pack_write(apck_pack_t *ctx, uint64_t offs, uint8_t *buf, uint64_t size)
{
    if(!ctx || !buf || !size) return APCK_ERR_BADINP;
    apck_fileseek(ctx->f, offs);
    return ctx->writecb ? (*ctx->writecb)(ctx->enc, ctx->f, offs, buf, size) : apck_filewrite(ctx->f, buf, size);
}

/**
 * Helper to sort assets
 */
static apck_pack_t *_apck_sort_ctx = NULL;
int _apck_pack_filecmp(const void *a, const void *b)
{
    apck_file_t *A = (apck_file_t*)a, *B = (apck_file_t*)b;
    if(!_apck_sort_ctx || !_apck_sort_ctx->strs) return 0;
    return strcmp(_apck_sort_ctx->strs + A->str, _apck_sort_ctx->strs + B->str);
}

/**
 * Finalize the archive, write it out and free internal buffers
 */
int apck_pack_flush(apck_pack_t *ctx)
{
    uint64_t offs, clen;
    uint32_t i, j, len;
    uint8_t *buf, *ptr, *comp;

    if(!ctx || !ctx->f) return APCK_ERR_BADINP;

    /* be paranoid, zero out length for missing buffers */
    if(!ctx->strs) ctx->strslen = 0;

    /* calculate maximum buffer length */
    len = 4 + ctx->strslen;
    for(i = 0; i < APCK_NUMTYPES; i++) {
        if(!ctx->files[i]) ctx->numfiles[i] = 0;
        len += ctx->numfiles[i] * 32 + 8;
    }

    /* allocate and construct asset directory */
    if(!(buf = (uint8_t*)malloc(len))) return APCK_ERR_NOMEM;
    *((uint32_t*)buf) = 4 + ctx->strslen;
    if(ctx->strs) memcpy(buf + 4, ctx->strs, ctx->strslen);

    /* for each directory */
    _apck_sort_ctx = ctx;
    for(ptr = buf + 4 + ctx->strslen, offs = 0, j = 0; j < APCK_NUMTYPES; j++) {
        ptr = apck_pack_uleb128(ptr, ctx->numfiles[j]);
        /* for each file in that directory */
        if(ctx->files[j]) {
            if(j < APCK_FONT && ctx->numfiles[j] > 1)
                qsort(&ctx->files[j][1], ctx->numfiles[j] - 1, sizeof(apck_file_t), _apck_pack_filecmp);
            else if(ctx->numfiles[j] && j != APCK_ATLAS)
                qsort(ctx->files[j], ctx->numfiles[j], sizeof(apck_file_t), _apck_pack_filecmp);
        }
        for(i = 0; i < ctx->numfiles[j]; i++)
            if(ctx->files[j][i].buf && ctx->files[j][i].size) {
                if(APCK_COMPRESSED(j, i)) {
                    DBG(("Compressing %2u/%s\n", j, ctx->strs ? ctx->strs + ctx->files[j][i].str : "..."));
                    clen = compressBound(ctx->files[j][i].size) + ctx->pad;
                    if((comp = (uint8_t*)malloc(clen))) {
                        compress2(comp, &clen, ctx->files[j][i].buf, ctx->files[j][i].size, 9);
                        ptr = apck_pack_uleb128(ptr, ctx->files[j][i].str + 4);
                        ptr = apck_pack_uleb128(ptr, offs);
                        ptr = apck_pack_uleb128(ptr, ctx->files[j][i].size);
                        ptr = apck_pack_uleb128(ptr, clen);
                        free(ctx->files[j][i].buf);
                        ctx->files[j][i].buf = comp;
                    } else {
                        free(buf);
                        return APCK_ERR_NOMEM;
                    }
                } else {
                    ptr = apck_pack_uleb128(ptr, ctx->files[j][i].str + 4);
                    ptr = apck_pack_uleb128(ptr, offs);
                    ptr = apck_pack_uleb128(ptr, ctx->files[j][i].size);
                    ptr = apck_pack_uleb128(ptr, 0);
                    clen = ctx->files[j][i].size;
                }
                if(ctx->pad) clen = (clen + ctx->pad - 1) & ~(ctx->pad - 1);
                ctx->files[j][i].size = clen;
                offs += clen;
            }
    }

    /* compress the asset directory */
    len = (uintptr_t)ptr - (uintptr_t)buf;
    clen = compressBound(len) + ctx->pad;
    if((comp = (uint8_t*)malloc(clen))) {
        memset(comp, 0, clen);
        compress2(comp, &clen, buf, len, 9);
        if(ctx->pad) clen = (clen + ctx->pad - 1) & ~(ctx->pad - 1);
        ctx->hdr.chksum = apck_crc32(buf, len);
        ctx->hdr.size = len;
        ctx->hdr.hdr_size = sizeof(apck_hdr_t) + clen;
    } else {
        free(buf);
        return APCK_ERR_NOMEM;
    }
    free(buf);

    /* write header */
    DBG(("Storing Header + Asset Directory (%u bytes)\n", ctx->hdr.hdr_size));
    apck_fileseek(ctx->f, ctx->offs);
    apck_filewrite(ctx->f, &ctx->hdr, sizeof(apck_hdr_t));

    /* write compressed (and possibly encrypted) asset directory */
    apck_pack_write(ctx, ctx->offs + sizeof(apck_hdr_t), comp, clen);
    free(comp);

    /* write out the compressed (and possibly encrypted) assets too */
    for(offs = sizeof(apck_hdr_t) + clen, j = 0; j < APCK_NUMTYPES; j++)
        if(ctx->files[j]) {
            for(i = 0; i < ctx->numfiles[j]; i++)
                if(ctx->files[j][i].buf) {
                    DBG(("Storing %2u/%s (at %x%08x, %u bytes)\n", j, ctx->strs ? ctx->strs + ctx->files[j][i].str : "...",
                        (uint32_t)(offs >> 32), (uint32_t)offs, (uint32_t)ctx->files[j][i].size));
                    apck_pack_write(ctx, ctx->offs + offs, ctx->files[j][i].buf, ctx->files[j][i].size);
                    free(ctx->files[j][i].buf);
                    offs += ctx->files[j][i].size;
                }
            free(ctx->files[j]);
        }
    DBG(("Done.\n"));

    /* free resources */
    if(ctx->enc) free(ctx->enc);
    if(ctx->strs) free(ctx->strs);
    if(ctx->strspos) free(ctx->strspos);
    memset(ctx, 0, sizeof(apck_pack_t));
    return APCK_OK;
}

/**
 * Add a string to the string table
 */
uint32_t apck_pack_str(apck_pack_t *ctx, char *name)
{
    uint32_t i, l;

    if(!ctx || !name || !*name)
        return 0;
    if(ctx->strs && ctx->strspos)
        for(i = 0; i < ctx->strsnum; i++)
            if(!strcmp(ctx->strs + ctx->strspos[i], name))
                return ctx->strspos[i];
    l = strlen(name);
    if(!ctx->strslen) ctx->strslen++;
    if(!(ctx->strs = (char*)realloc(ctx->strs, ctx->strslen + l + 1)) ||
       !(ctx->strspos = (uint32_t*)realloc(ctx->strspos, (ctx->strsnum + 1) * sizeof(uint32_t)))) {
        ctx->strslen = ctx->strsnum = 0;
        return 0;
    }
    if(ctx->strslen == 1) ctx->strs[0] = 0;
    ctx->strspos[ctx->strsnum++] = i = ctx->strslen;
    strcpy(ctx->strs + ctx->strslen, name);
    ctx->strslen += l + 1;
    return i;
}

/**
 * Add a raw asset (do not use directly, use apck_pack_asset() instead)
 */
int apck_pack_add(apck_pack_t *ctx, int type, char *name, uint8_t *buf, uint64_t size)
{
    uint32_t i;

    if(!ctx || !buf || !size) return APCK_ERR_BADINP;

    if(type < 0 || type >= APCK_NUMTYPES || size < 4 ||
        (type < APCK_FONT && ctx->numfiles[type] > 0 && memcmp(buf, "\x89PNG", 4) && memcmp(buf, "OggS", 4)) ||
        ((type == APCK_IMAGE || type == APCK_ATLAS) && memcmp(buf, "\x89PNG", 4)) ||
        ((type == APCK_BGM || type == APCK_SFX || type == APCK_VIDEO) && memcmp(buf, "OggS", 4)) ||
        (type == APCK_FONT && memcmp(buf, "SFN2", 4)) || (type == APCK_MODEL && memcmp(buf, "3DMO", 4))) {
            DBG(("unsupported file format %3u/%s, unable to add\n", type, name));
            return APCK_ERR_BADINP;
        }

    i = ctx->numfiles[type]++;
    if(!(ctx->files[type] = (apck_file_t*)realloc(ctx->files[type], ctx->numfiles[type] * sizeof(apck_file_t)))) {
        ctx->numfiles[type] = 0;
        return APCK_ERR_NOMEM;
    }
    memset(&ctx->files[type][i], 0, sizeof(apck_file_t));
    ctx->files[type][i].buf = buf;
    ctx->files[type][i].size = size;
    ctx->files[type][i].str = apck_pack_str(ctx, name);
    return APCK_OK;
}

/**
 * Encode a 32-bit RGBA pixel buffer into an image asset
 */
#ifdef PNG_H
typedef struct {
    uint8_t *buf;
    int len;
} apck_image_png_t;
void apck_image_write(png_structp png_ptr, png_bytep data, png_size_t len)
{
    apck_image_png_t *pngbuf = (apck_image_png_t*)png_get_io_ptr(png_ptr);
    pngbuf->buf = (uint8_t*)realloc(pngbuf->buf, pngbuf->len + len);
    if(!pngbuf->buf) { pngbuf->len = 0; return; }
    memcpy(pngbuf->buf + pngbuf->len, data, len);
    pngbuf->len += len;
}
void apck_image_flush(png_structp png_ptr) { (void)png_ptr; }
#endif
uint8_t *apck_pack_png(uint32_t *buf, int w, int h, int p, uint64_t *size)
{
#ifdef APCK_PACK_PNG
    return APCK_PACK_PNG(buf, w, h, p, size);
#elif defined(INCLUDE_STB_IMAGE_WRITE_H)
    uint8_t *ret = NULL;
    int s;
    stbi_write_png_compression_level = 9;
    ret = stbi_write_png_to_mem((uint8_t*)buf, p, w, h, 4, &s);
    if(size && ret) *size = (uint64_t)s;
    return ret;
#elif defined(PNG_H)
    apck_image_png_t pngbuf = { 0 };
    uint32_t *ptr = buf, pal[256];
    uint8_t *buf1 = (uint8_t*)buf, *buf2, *pal2 = (uint8_t*)&pal;
    png_color pngpal[256];
    png_byte pngtrn[256];
    png_structp png_ptr;
    png_infop info_ptr;
    png_bytep *rows;
    int i, j, nc = 0;

    if(!buf || !size || w < 1 || h < 1 || p < w * 4) return NULL;
    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if(!png_ptr) return NULL;
    info_ptr = png_create_info_struct(png_ptr);
    if(!info_ptr) { png_destroy_write_struct(&png_ptr, NULL); return NULL; }
    if(setjmp(png_jmpbuf(png_ptr))) { png_destroy_write_struct(&png_ptr, &info_ptr); return NULL; }
    png_set_write_fn(png_ptr, (void*)&pngbuf, apck_image_write, apck_image_flush);
    /* aim for small file size, even if it means slower encoding */
    png_set_compression_level(png_ptr, 9);
    png_set_compression_strategy(png_ptr, 0);
    png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_FILTER_VALUE_SUB);
    if(!(rows = (png_bytep*)malloc(h * sizeof(png_bytep)))) return NULL;
    if(!(buf2 = (uint8_t*)malloc(w * h))) { free(rows); return NULL; }
    /* lets see if we can save this as an indexed image */
    for(i = 0; i < w * h; i++) {
        for(j = 0; j < nc && pal[j] != ptr[i]; j++);
        if(j >= nc) {
            if(nc == 256) { nc = -1; break; }
            pal[nc++] = ptr[i];
        }
        buf2[i] = j;
    }
    if(nc != -1) {
        for(i = j = 0; i < nc; i++) {
            pngpal[i].red = pal2[i * 4 + 0];
            pngpal[i].green = pal2[i * 4 + 1];
            pngpal[i].blue = pal2[i * 4 + 2];
            pngtrn[i] = pal2[i * 4 + 3];
        }
        png_set_PLTE(png_ptr, info_ptr, pngpal, nc);
        png_set_tRNS(png_ptr, info_ptr, pngtrn, nc, NULL);
        for(i = 0; i < h; i++) rows[i] = buf2 + i * w;
    } else
        for(i = 0; i < h; i++) rows[i] = buf1 + i * p;
    png_set_IHDR(png_ptr, info_ptr, w, h, 8, nc == -1 ? PNG_COLOR_TYPE_RGB_ALPHA : PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
        PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
    png_write_info(png_ptr, info_ptr);
    png_write_image(png_ptr, rows);
    png_write_end(png_ptr, info_ptr);
    png_destroy_write_struct(&png_ptr, &info_ptr);
    free(rows);
    free(buf2);
    *size = pngbuf.len;
    return pngbuf.buf;
#else
#error "Need stb_image_write.h, libpng or custom png implementation"
#endif
}

/**
 * Encode PCM data into OGG
 */
uint8_t *apck_pack_ogg(uint8_t *buf, uint64_t len, uint64_t *size)
{
#ifdef APCK_PACK_OGG
    return APCK_PACK_OGG(buf, len, size);
#elif defined(_OV_ENC_H_)
#define OGGREAD 1024
    ogg_stream_state os;
    ogg_page         og;
    ogg_packet       op;
    ogg_packet       header;
    ogg_packet       header_comm;
    ogg_packet       header_code;
    vorbis_info      vi;
    vorbis_comment   vc;
    vorbis_dsp_state vd;
    vorbis_block     vb;
    int64_t i, j, numsample;
    int eos=0;
    uint64_t outsize = 0;
    uint16_t channels;
    uint8_t *out = NULL, *ptr = buf + 44, *end = buf + len;
    float **buffer;

    if(!buf || len < 44 || !size || memcmp(buf, "RIFF", 4) || *((uint16_t*)(buf + 22)) < 1)
        return NULL;
    channels = *((uint16_t*)(buf + 22));
    *size = 0;

    vorbis_info_init(&vi);
    if(vorbis_encode_init_vbr(&vi,channels,*((int32_t*)(buf + 24)),0.4))
        return NULL;
    vorbis_comment_init(&vc);
    vorbis_analysis_init(&vd,&vi);
    vorbis_block_init(&vd,&vb);
    ogg_stream_init(&os,1337);
    vorbis_analysis_headerout(&vd,&vc,&header,&header_comm,&header_code);
    ogg_stream_packetin(&os,&header);
    ogg_stream_packetin(&os,&header_comm);
    ogg_stream_packetin(&os,&header_code);
    while(!eos){
        if(!ogg_stream_flush(&os,&og)) break;
        if(!(out = (uint8_t*)realloc(out, outsize + og.header_len + og.body_len)))
            goto fail;
        memcpy(out + outsize, og.header, og.header_len); outsize += og.header_len;
        memcpy(out + outsize, og.body, og.body_len); outsize += og.body_len;
    }
    while(!eos){
        numsample = ((uintptr_t)end - (uintptr_t)ptr) / channels / sizeof(int16_t);
        if(numsample > OGGREAD) numsample = OGGREAD;
        if(!numsample) {
            vorbis_analysis_wrote(&vd,0);
        } else {
            buffer=vorbis_analysis_buffer(&vd,OGGREAD);
            for(i = 0; i < numsample; i++)
                for(j = 0; j < channels; j++)
                    buffer[j][i] = ((ptr[(i * channels + j) * sizeof(int16_t) + 1] << 8) |
                        (0x00ff & (int)ptr[(i * channels + j) * sizeof(int16_t) + 0])) / 32768.f;
            ptr += numsample * channels * sizeof(int16_t);
            vorbis_analysis_wrote(&vd,numsample);
        }
        while(vorbis_analysis_blockout(&vd,&vb)==1){
            vorbis_analysis(&vb,NULL);
            vorbis_bitrate_addblock(&vb);
            while(vorbis_bitrate_flushpacket(&vd,&op)){
                ogg_stream_packetin(&os,&op);
                while(!eos){
                    if(!ogg_stream_pageout(&os,&og)) break;
                    if(!(out = (uint8_t*)realloc(out, outsize + og.header_len + og.body_len)))
                        goto fail;
                    memcpy(out + outsize, og.header, og.header_len); outsize += og.header_len;
                    memcpy(out + outsize, og.body, og.body_len); outsize += og.body_len;
                    if(ogg_page_eos(&og))eos=1;
                }
            }
        }
    }
    ogg_stream_clear(&os);
    vorbis_block_clear(&vb);
    vorbis_dsp_clear(&vd);
    vorbis_comment_clear(&vc);
    vorbis_info_clear(&vi);
    *size = outsize;
    return out;
fail:
    ogg_stream_clear(&os);
    vorbis_block_clear(&vb);
    vorbis_dsp_clear(&vd);
    vorbis_comment_clear(&vc);
    vorbis_info_clear(&vi);
    return NULL;
#undef OGGREAD
#else
#error "Need libvorbisenc or custom ogg+vorbis implementation"
#endif
}

/**
 * Serialize a map asset
 */
uint8_t *apck_map_encode(apck_pack_t *ctx, apck_map_t *map, uint64_t *size)
{
    apck_map_list_t *list;
    uint8_t *buf, *ptr, *ptr2;
    uint32_t *data;
    int i, j, k, l, o, n;

    if(!ctx || !map || !size || !(buf = (uint8_t*)malloc(4 + 6 * 8 + map->proplen + map->l * (9 * 8 + map->w * map->h * 5))))
        return NULL;
    if(!map->layers) map->l = 0;

    /* header */
    memcpy(buf, "MAP", 3); buf[3] = map->type;
    ptr = buf + 4;
    ptr = apck_pack_uleb128(ptr, map->tilew);
    ptr = apck_pack_uleb128(ptr, map->tileh);
    ptr = apck_pack_uleb128(ptr, map->w);
    ptr = apck_pack_uleb128(ptr, map->h);
    ptr = apck_pack_uleb128(ptr, map->l);
    if(map->proplen && map->properties) {
        ptr = apck_pack_uleb128(ptr, map->proplen);
        memcpy(ptr, map->properties, map->proplen);
        ptr += map->proplen;
    } else
        ptr = apck_pack_uleb128(ptr, 0);

    /* layers */
    for(j = 0; j < map->l; j++) {
        ptr = apck_pack_uleb128(ptr, map->layers[j].type);
        ptr = apck_pack_uleb128(ptr, map->layers[j].disptype);
        ptr = apck_pack_uleb128(ptr, map->layers[j].offX);
        ptr = apck_pack_uleb128(ptr, map->layers[j].offY);
        ptr = apck_pack_uleb128(ptr, map->layers[j].parX);
        ptr = apck_pack_uleb128(ptr, map->layers[j].parY);
        ptr = apck_pack_uleb128(ptr, map->layers[j].movX + 100);
        ptr = apck_pack_uleb128(ptr, map->layers[j].movY + 100);
        if(map->layers[j].data)
            switch(map->layers[j].type) {
                case APCK_MAP:
                    data = (uint32_t*)map->layers[j].data;
                    n = map->w * map->h;
                    k = o = 0; ptr[o++] = 0;
                    for(i = 0; i < n; i++) {
                        for(l = 1; l < 128 && i + l < n && data[i] == data[i + l]; l++);
                        if(l > 1) {
                            l--; if(ptr[k]) { ptr[k]--; ptr[o++] = 0x80 | l; } else ptr[k] = 0x80 | l;
                            ptr2 = apck_pack_uleb128(ptr + o, data[i]); o = ptr2 - ptr;
                            k = o; ptr[o++] = 0; i += l; continue;
                        }
                        ptr[k]++; ptr2 = apck_pack_uleb128(ptr + o, data[i]); o = ptr2 - ptr;
                        if(ptr[k] > 127) { ptr[k]--; k = o; ptr[o++] = 0; }
                    }
                    if(!(ptr[k] & 0x80)) { if(ptr[k]) ptr[k]--; else o--; }
                    ptr += o;
                break;
                case APCK_SPRITE:
                    list = (apck_map_list_t*)map->layers[j].data;
                    n = map->layers[j].size / sizeof(apck_map_list_t);
                    ptr = apck_pack_uleb128(ptr, n);
                    for(i = 0; i < n; i++) {
                        ptr = apck_pack_uleb128(ptr, apck_pack_str(ctx, (char*)list[i].v));
                        ptr = apck_pack_uleb128(ptr, list[i].x);
                        ptr = apck_pack_uleb128(ptr, list[i].y);
                    }
                break;
                default:
                    if(map->layers[j].type >= APCK_OBJECT) {
                        ptr = apck_pack_uleb128(ptr, map->layers[j].size);
                        memcpy(ptr, map->layers[j].data, map->layers[j].size);
                        ptr += map->layers[j].size;
                    } else
                        ptr = apck_pack_uleb128(ptr, apck_pack_str(ctx, (char*)map->layers[j].data));
                break;
            }
        else
            ptr = apck_pack_uleb128(ptr, 0);
    }
    *size = (uintptr_t)ptr - (uintptr_t)buf;
    buf = (uint8_t*)realloc(buf, *size);
    return buf;
}

/**
 * Serialize a sprite asset
 */
uint8_t *apck_sprite_encode(apck_pack_t *ctx, apck_sprite_t *sprite, uint64_t *size)
{
    uint8_t *buf, *ptr;
    int i, l;

    if(!ctx || !sprite || !size)
        return NULL;
    for(i = l = 0; i < APCK_NUMDIR; i++)
        l += 4 + sprite->nf[i] * 40;
    if(!(buf = (uint8_t*)malloc(4 + 3 * 4 + l)))
        return NULL;

    /* header */
    memcpy(buf, "SPFR", 4);
    ptr = buf + 4;
    ptr = apck_pack_uleb128(ptr, sprite->type);
    ptr = apck_pack_uleb128(ptr, sprite->w);
    ptr = apck_pack_uleb128(ptr, sprite->h);

    /* directions */
    for(l = 0; l < APCK_NUMDIR; l++)
        if(sprite->frames[l]) {
            ptr = apck_pack_uleb128(ptr, sprite->nf[l]);
            /* frames */
            for(i = 0; i < sprite->nf[l]; i++) {
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].atlas & 0xff);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].m);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].w);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].h);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].sx);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].sy);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].dx);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].dy);
                ptr = apck_pack_uleb128(ptr, sprite->frames[l][i].dur);
                ptr = apck_pack_uleb128(ptr, apck_pack_str(ctx, sprite->frames[l][i].sfx));
            }
        } else
            ptr = apck_pack_uleb128(ptr, 0);

    *size = (uintptr_t)ptr - (uintptr_t)buf;
    buf = (uint8_t*)realloc(buf, *size);
    return buf;
}

/**
 * Add an asset to the archive
 */
int apck_pack_asset(apck_pack_t *ctx, int type, char *name, void *buf, uint64_t size, int w, int h)
{
    uint64_t len = 0;
    uint32_t *pix;
    uint8_t *data;
    char tmp[32];
    int i;

    if(!ctx || type >= APCK_NUMTYPES || (type != APCK_ATLAS && (!name || !*name)) || !buf || !size) return APCK_ERR_BADINP;

    sprintf(tmp, "%04u", ctx->numfiles[type] + 1);
    DBG(("Adding %2u/%s\n", type, name ? name : tmp));
    switch(type) {
        case APCK_BGM:
        case APCK_SFX:
audio:      if(!memcmp(buf, "RIFF", 4)) {
                if((data = apck_pack_ogg(buf, size, &len)))
                    return apck_pack_add(ctx, type, name, data, len);
                else
                    return APCK_ERR_NOMEM;
            }
        break;
        case APCK_ATLAS:
            /* crop bottom */
            for(pix = (uint32_t*)buf, i = w * h - 1; i > 0 && !(pix[i] & 0xfc000000); i--);
            h = (i + w - 1) / w;
            goto image;
        case APCK_IMAGE:
image:      if(w > 0 && h > 0) {
                if((data = apck_pack_png((uint32_t*)buf, w, h, w * 4, &len)))
                    return apck_pack_add(ctx, type, name, data, len);
                else
                    return APCK_ERR_NOMEM;
            }
        break;
        case APCK_SPRITE:
            if((data = apck_sprite_encode(ctx, (apck_sprite_t*)buf, &len)))
                return apck_pack_add(ctx, type, name, data, len);
            else
                return APCK_ERR_NOMEM;
        break;
        case APCK_MAP:
            if((data = apck_map_encode(ctx, (apck_map_t*)buf, &len)))
                return apck_pack_add(ctx, type, name, data, len);
            else
                return APCK_ERR_NOMEM;
        break;
        default:
            if(type < APCK_FONT && ctx->numfiles[type]) {
                if(w > 0 && h > 0) goto image; else
                if(!memcmp(buf, "RIFF", 4)) goto audio; else
                return APCK_ERR_BADINP;
            }
            return apck_pack_add(ctx, type, name, buf, size);
    }
    return APCK_ERR_BADINP;
}

#endif /* APCK_PACK_IMPLEMENTATION */

#ifdef  __cplusplus
}
#endif
#endif /* APIC_PACK_H */
