/* gi_blorb.c: Blorb library layer for Glk API.
    gi_blorb version 1.4.
    Designed by Andrew Plotkin <erkyrath@eblong.com>
    http://www.eblong.com/zarf/glk/index.html

    This file is copyright 1998-2000 by Andrew Plotkin. You may copy,
    distribute, and incorporate it into your own programs, by any means
    and under any conditions, as long as you do not modify it. You may
    also modify this file, incorporate it into your own programs,
    and distribute the modified version, as long as you retain a notice
    in your program or documentation which mentions my name and the URL
    shown above.
*/

#include "glk.h"
#include "gi_blorb.h"

#ifndef NULL
#define NULL 0
#endif
#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif

/* The magic macro of endian conversion. */

#define giblorb_native4(v)   \
    ( (((glui32)((v)[3])      ) & 0x000000ff)    \
    | (((glui32)((v)[2]) <<  8) & 0x0000ff00)    \
    | (((glui32)((v)[1]) << 16) & 0x00ff0000)    \
    | (((glui32)((v)[0]) << 24) & 0xff000000))

/* More four-byte constants. */

#define giblorb_ID_FORM (giblorb_make_id('F', 'O', 'R', 'M'))
#define giblorb_ID_IFRS (giblorb_make_id('I', 'F', 'R', 'S'))
#define giblorb_ID_RIdx (giblorb_make_id('R', 'I', 'd', 'x'))

/* giblorb_chunkdesc_t: Describes one chunk of the Blorb file. */
typedef struct giblorb_chunkdesc_struct {
    glui32 type;
    glui32 len;
    glui32 startpos; /* start of chunk header */
    glui32 datpos; /* start of data (either startpos or startpos+8) */
    
    void *ptr; /* pointer to malloc'd data, if loaded */
    int auxdatnum; /* entry in the auxsound/auxpict array; -1 if none.
        This only applies to chunks that represent resources;  */
    
} giblorb_chunkdesc_t;

/* giblorb_resdesc_t: Describes one resource in the Blorb file. */
typedef struct giblorb_resdesc_struct {
    glui32 usage;
    glui32 resnum;
    glui32 chunknum;
} giblorb_resdesc_t;

/* giblorb_map_t: Holds the complete description of an open Blorb file. */
struct giblorb_map_struct {
    glui32 inited; /* holds giblorb_Inited_Magic if the map structure is 
        valid */
    strid_t file;
    
    int numchunks;
    giblorb_chunkdesc_t *chunks; /* list of chunk descriptors */
    
    int numresources;
    giblorb_resdesc_t *resources; /* list of resource descriptors */
    giblorb_resdesc_t **ressorted; /* list of pointers to descriptors 
        in map->resources -- sorted by usage and resource number. */
};

#define giblorb_Inited_Magic (0xB7012BED) 

/* Static variables. */

static int lib_inited = FALSE;

static giblorb_err_t giblorb_initialize(void);
static giblorb_err_t giblorb_initialize_map(giblorb_map_t *map);
static void giblorb_qsort(giblorb_resdesc_t **list, int len);
static giblorb_resdesc_t *giblorb_bsearch(giblorb_resdesc_t *sample, 
    giblorb_resdesc_t **list, int len);
static void *giblorb_malloc(glui32 len);
static void *giblorb_realloc(void *ptr, glui32 len);
static void giblorb_free(void *ptr);

static giblorb_err_t giblorb_initialize()
{
    return giblorb_err_None;
}

giblorb_err_t giblorb_create_map(strid_t file, giblorb_map_t **newmap)
{
    giblorb_err_t err;
    giblorb_map_t *map;
    glui32 readlen;
    glui32 nextpos, totallength;
    giblorb_chunkdesc_t *chunks;
    int chunks_size, numchunks;
    char buffer[16];
    
    *newmap = NULL;
    
    if (!lib_inited) {
        err = giblorb_initialize();
        if (err)
            return err;
        lib_inited = TRUE;
    }

    /* First, chew through the file and index the chunks. */
    
    glk_stream_set_position(file, 0, seekmode_Start);
    
    readlen = glk_get_buffer_stream(file, buffer, 12);
    if (readlen != 12)
        return giblorb_err_Read;
    
    if (giblorb_native4(buffer+0) != giblorb_ID_FORM)
        return giblorb_err_Format;
    if (giblorb_native4(buffer+8) != giblorb_ID_IFRS)
        return giblorb_err_Format;
    
    totallength = giblorb_native4(buffer+4) + 8;
    nextpos = 12;

    chunks_size = 8;
    numchunks = 0;
    chunks = (giblorb_chunkdesc_t *)giblorb_malloc(sizeof(giblorb_chunkdesc_t) 
        * chunks_size);

    while (nextpos < totallength) {
        glui32 type, len;
        int chunum;
        giblorb_chunkdesc_t *chu;
        
        glk_stream_set_position(file, nextpos, seekmode_Start);
        
        readlen = glk_get_buffer_stream(file, buffer, 8);
        if (readlen != 8)
            return giblorb_err_Read;
        
        type = giblorb_native4(buffer+0);
        len = giblorb_native4(buffer+4);
        
        if (numchunks >= chunks_size) {
            chunks_size *= 2;
            chunks = (giblorb_chunkdesc_t *)giblorb_realloc(chunks, 
                sizeof(giblorb_chunkdesc_t) * chunks_size);
        }
        
        chunum = numchunks;
        chu = &(chunks[chunum]);
        numchunks++;
        
        chu->type = type;
        chu->startpos = nextpos;
        if (type == giblorb_ID_FORM) {
            chu->datpos = nextpos;
            chu->len = len+8;
        }
        else {
            chu->datpos = nextpos+8;
            chu->len = len;
        }
        chu->ptr = NULL;
        chu->auxdatnum = -1;
        
        nextpos = nextpos + len + 8;
        if (nextpos & 1)
            nextpos++;
            
        if (nextpos > totallength)
            return giblorb_err_Format;
    }
    
    /* The basic IFF structure seems to be ok, and we have a list of
        chunks. Now we allocate the map structure itself. */
    
    map = (giblorb_map_t *)giblorb_malloc(sizeof(giblorb_map_t));
    if (!map) {
        giblorb_free(chunks);
        return giblorb_err_Alloc;
    }
        
    map->inited = giblorb_Inited_Magic;
    map->file = file;
    map->chunks = chunks;
    map->numchunks = numchunks;
    map->resources = NULL;
    map->ressorted = NULL;
    map->numresources = 0;
    /*map->releasenum = 0;
    map->zheader = NULL;
    map->resolution = NULL;
    map->palettechunk = -1;
    map->palette = NULL;
    map->auxsound = NULL;
    map->auxpict = NULL;*/
    
    /* Now we do everything else involved in loading the Blorb file,
        such as building resource lists. */
    
    err = giblorb_initialize_map(map);
    if (err) {
        giblorb_destroy_map(map);
        return err;
    }
    
    *newmap = map;
    return giblorb_err_None;
}

static giblorb_err_t giblorb_initialize_map(giblorb_map_t *map)
{
    /* It is important that the map structure be kept valid during this
        function. If this returns an error, giblorb_destroy_map() will 
        be called. */
        
    int ix, jx;
    giblorb_result_t chunkres;
    giblorb_err_t err;
    char *ptr;
    glui32 len;
    glui32 val;
    glui32 numres;
    int gotindex = FALSE; 

    for (ix=0; ix<map->numchunks; ix++) {
        giblorb_chunkdesc_t *chu = &map->chunks[ix];
        
        switch (chu->type) {
        
            case giblorb_ID_RIdx:
                /* Resource index chunk: build the resource list and 
                sort it. */
                
                if (gotindex) 
                    return giblorb_err_Format; /* duplicate index chunk */
                err = giblorb_load_chunk_by_number(map, giblorb_method_Memory, 
                    &chunkres, ix);
                if (err) 
                    return err;
                
                ptr = chunkres.data.ptr;
                len = chunkres.length;
                numres = giblorb_native4(ptr+0);

                if (numres) {
                    int ix2;
                    giblorb_resdesc_t *resources;
                    giblorb_resdesc_t **ressorted;
                    
                    if (len != numres*12+4)
                        return giblorb_err_Format; /* bad length field */
                    
                    resources = (giblorb_resdesc_t *)giblorb_malloc(numres 
                        * sizeof(giblorb_resdesc_t));
                    ressorted = (giblorb_resdesc_t **)giblorb_malloc(numres 
                        * sizeof(giblorb_resdesc_t *));
                    if (!ressorted || !resources)
                        return giblorb_err_Alloc;
                    
                    ix2 = 0;
                    for (jx=0; jx<numres; jx++) {
                        giblorb_resdesc_t *res = &(resources[jx]);
                        glui32 respos;
                        
                        res->usage = giblorb_native4(ptr+jx*12+4);
                        res->resnum = giblorb_native4(ptr+jx*12+8);
                        respos = giblorb_native4(ptr+jx*12+12);
                        
                        while (ix2 < map->numchunks 
                            && map->chunks[ix2].startpos < respos)
                            ix2++;
                        
                        if (ix2 >= map->numchunks 
                            || map->chunks[ix2].startpos != respos)
                            return giblorb_err_Format; /* start pos does  
                                not match a real chunk */
                        
                        res->chunknum = ix2;
                        
                        ressorted[jx] = res;
                    }
                    
                    /* Sort a resource list (actually a list of pointers to 
                        structures in map->resources.) This makes it easy 
                        to find resources by usage and resource number. */
                    giblorb_qsort(ressorted, numres);
                    
                    map->numresources = numres;
                    map->resources = resources;
                    map->ressorted = ressorted;
                }
                
                giblorb_unload_chunk(map, ix);
                gotindex = TRUE;
                break;
            
        }
    }
    
    return giblorb_err_None;
}

giblorb_err_t giblorb_destroy_map(giblorb_map_t *map)
{
    int ix;
    
    if (!map || !map->chunks || map->inited != giblorb_Inited_Magic)
        return giblorb_err_NotAMap;
    
    for (ix=0; ix<map->numchunks; ix++) {
        giblorb_chunkdesc_t *chu = &(map->chunks[ix]);
        if (chu->ptr) {
            giblorb_free(chu->ptr);
            chu->ptr = NULL;
        }
    }
    
    if (map->chunks) {
        giblorb_free(map->chunks);
        map->chunks = NULL;
    }
    
    map->numchunks = 0;
    
    if (map->resources) {
        giblorb_free(map->resources);
        map->resources = NULL;
    }
    
    if (map->ressorted) {
        giblorb_free(map->ressorted);
        map->ressorted = NULL;
    }
    
    map->numresources = 0;
    
    map->file = NULL;
    map->inited = 0;
    
    giblorb_free(map);

    return giblorb_err_None;
}

/* Chunk-handling functions. */

giblorb_err_t giblorb_load_chunk_by_type(giblorb_map_t *map, 
    glui32 method, giblorb_result_t *res, glui32 type, 
    glui32 count)
{
    int ix;
    
    for (ix=0; ix < map->numchunks; ix++) {
        if (map->chunks[ix].type == type) {
            if (count == 0)
                break;
            count--;
        }
    }
    
    if (ix >= map->numchunks) {
        return giblorb_err_NotFound;
    }
    
    return giblorb_load_chunk_by_number(map, method, res, ix);
}

giblorb_err_t giblorb_load_chunk_by_number(giblorb_map_t *map, 
    glui32 method, giblorb_result_t *res, glui32 chunknum)
{
    giblorb_chunkdesc_t *chu;
    
    if (chunknum < 0 || chunknum >= map->numchunks)
        return giblorb_err_NotFound;

    chu = &(map->chunks[chunknum]);
    
    switch (method) {
    
        case giblorb_method_DontLoad:
            /* do nothing */
            break;
            
        case giblorb_method_FilePos:
            res->data.startpos = chu->datpos;
            break;
            
        case giblorb_method_Memory:
            if (!chu->ptr) {
                giblorb_err_t err;
                glui32 readlen;
                void *dat = giblorb_malloc(chu->len);
                
                if (!dat)
                    return giblorb_err_Alloc;
                
                glk_stream_set_position(map->file, chu->datpos, 
                    seekmode_Start);
                
                readlen = glk_get_buffer_stream(map->file, dat, 
                    chu->len);
                if (readlen != chu->len)
                    return giblorb_err_Read;
                
                chu->ptr = dat;
            }
            res->data.ptr = chu->ptr;
            break;
    }
    
    res->chunknum = chunknum;
    res->length = chu->len;
    res->chunktype = chu->type;
    
    return giblorb_err_None;
}

giblorb_err_t giblorb_load_resource(giblorb_map_t *map, glui32 method, 
    giblorb_result_t *res, glui32 usage, glui32 resnum)
{
    giblorb_resdesc_t sample;
    giblorb_resdesc_t *found;
    
    sample.usage = usage;
    sample.resnum = resnum;
    
    found = giblorb_bsearch(&sample, map->ressorted, map->numresources);
    
    if (!found)
        return giblorb_err_NotFound;
    
    return giblorb_load_chunk_by_number(map, method, res, found->chunknum);
}

giblorb_err_t giblorb_unload_chunk(giblorb_map_t *map, glui32 chunknum)
{
    giblorb_chunkdesc_t *chu;
    
    if (chunknum < 0 || chunknum >= map->numchunks)
        return giblorb_err_NotFound;

    chu = &(map->chunks[chunknum]);
    
    if (chu->ptr) {
        giblorb_free(chu->ptr);
        chu->ptr = NULL;
    }
    
    return giblorb_err_None;
}

giblorb_err_t giblorb_count_resources(giblorb_map_t *map, glui32 usage,
    glui32 *num, glui32 *min, glui32 *max)
{
    int ix;
    int count;
    glui32 val;
    glui32 minval, maxval;
    
    count = 0;
    minval = 0;
    maxval = 0;
    
    for (ix=0; ix<map->numresources; ix++) {
        if (map->resources[ix].usage == usage) {
            val = map->resources[ix].resnum;
            if (count == 0) {
                count++;
                minval = val;
                maxval = val;
            }
            else {
                count++;
                if (val < minval)
                    minval = val;
                if (val > maxval)
                    maxval = val;
            }
        }
    }
    
    if (num)
        *num = count;
    if (min)
        *min = minval;
    if (max)
        *max = maxval;
    
    return giblorb_err_None;
}

/* Sorting and searching. */

static int sortsplot(giblorb_resdesc_t *v1, giblorb_resdesc_t *v2)
{
    if (v1->usage < v2->usage)
        return -1;
    if (v1->usage > v2->usage)
        return 1;
    if (v1->resnum < v2->resnum)
        return -1;
    if (v1->resnum > v2->resnum)
        return 1;
    return 0;
}

static void giblorb_qsort(giblorb_resdesc_t **list, int len)
{
    int ix, jx, res, val;
    giblorb_resdesc_t *tmpptr, *pivot;
    
    if (len < 6) {
        /* The list is short enough for a bubble-sort. */
        for (jx=len-1; jx>0; jx--) {
            for (ix=0; ix<jx; ix++) {
                res = sortsplot(list[ix], list[ix+1]);
                if (res > 0) {
                    tmpptr = list[ix];
                    list[ix] = list[ix+1];
                    list[ix+1] = tmpptr;
                }
            }
        }
    }
    else {
        /* Split the list. */
        pivot = list[len/2];
        ix=0;
        jx=len;
        while (1) {
            while (ix < jx-1 && sortsplot(list[ix], pivot) < 0)
                ix++;
            while (ix < jx-1 && sortsplot(list[jx-1], pivot) > 0)
                jx--;
            if (ix >= jx-1)
                break;
            tmpptr = list[ix];
            list[ix] = list[jx-1];
            list[jx-1] = tmpptr;
        }
        ix++;
        /* Sort the halves. */
        giblorb_qsort(list+0, ix);
        giblorb_qsort(list+ix, len-ix);
    }
}

giblorb_resdesc_t *giblorb_bsearch(giblorb_resdesc_t *sample, 
    giblorb_resdesc_t **list, int len)
{
    int top, bot, val, res;
    
    bot = 0;
    top = len;
    
    while (bot < top) {
        val = (top+bot) / 2;
        res = sortsplot(list[val], sample);
        if (res == 0)
            return list[val];
        if (res < 0) {
            bot = val+1;
        }
        else {
            top = val;
        }
    }
    
    return NULL;
}


/* Boring utility functions. If your platform doesn't support ANSI 
    malloc(), feel free to edit these however you like. */

#include <stdlib.h> /* The OS-native header file -- you can edit 
    this too. */

static void *giblorb_malloc(glui32 len)
{
    return malloc(len);
}

static void *giblorb_realloc(void *ptr, glui32 len)
{
    return realloc(ptr, len);
}

static void giblorb_free(void *ptr)
{
    free(ptr);
}


