/* gi_blorb.c: Blorb library layer for Glk API.
   gi_blorb version 1.6.0.
   Designed by Andrew Plotkin <[email protected]>
   http://eblong.com/zarf/glk/

   This file is copyright 1998-2017 by Andrew Plotkin. It is
   distributed under the MIT license; see the "LICENSE" file.
*/

#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.
       (Currently, only images.) */

} 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_auxpict_t: Extra information about an image. */
typedef struct giblorb_auxpict_struct {
   int loaded;
   glui32 width;
   glui32 height;
   char *alttext;
} giblorb_auxpict_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. */

   giblorb_auxpict_t *auxpict;
};

#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 giblorb_err_t giblorb_image_get_size_jpeg(unsigned char *ptr, glui32 length, giblorb_auxpict_t *auxpict);
static giblorb_err_t giblorb_image_get_size_png(unsigned char *ptr, glui32 length, giblorb_auxpict_t *auxpict);
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) {
           giblorb_free(chunks);
           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) {
           giblorb_free(chunks);
           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 numres;
   int gotindex = FALSE;
   int pictcount = 0;

   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 = NULL;
                   giblorb_resdesc_t **ressorted = NULL;

                   if (len != numres*12+4)
                       return giblorb_err_Format; /* bad length field */

                   resources = (giblorb_resdesc_t *)giblorb_malloc(numres
                       * sizeof(giblorb_resdesc_t));
                   if (!resources) {
                       return giblorb_err_Alloc;
                   }
                   ressorted = (giblorb_resdesc_t **)giblorb_malloc(numres
                       * sizeof(giblorb_resdesc_t *));
                   if (!ressorted) {
                       giblorb_free(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) {
                           /* start pos does not match a real chunk */
                           giblorb_free(resources);
                           giblorb_free(ressorted);
                           return giblorb_err_Format;
                       }

                       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;

           case giblorb_ID_JPEG:
           case giblorb_ID_PNG:
               chu->auxdatnum = pictcount;
               pictcount++;
               break;

       }
   }

   if (pictcount) {
       map->auxpict = (giblorb_auxpict_t *)giblorb_malloc(pictcount
           * sizeof(giblorb_auxpict_t));
       if (!map->auxpict)
           return giblorb_err_Alloc;
       for (ix=0; ix<pictcount; ix++) {
           giblorb_auxpict_t *auxpict = &(map->auxpict[ix]);
           auxpict->loaded = FALSE;
           auxpict->width = 0;
           auxpict->height = 0;
           auxpict->alttext = NULL;
       }
   }

   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;

   if (map->auxpict) {
       giblorb_free(map->auxpict);
       map->auxpict = NULL;
   }

   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 >= 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) {
               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 >= 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;
}

giblorb_err_t giblorb_load_image_info(giblorb_map_t *map,
   glui32 resnum, giblorb_image_info_t *res)
{
   giblorb_resdesc_t sample;
   giblorb_resdesc_t *found;

   sample.usage = giblorb_ID_Pict;
   sample.resnum = resnum;

   found = giblorb_bsearch(&sample, map->ressorted, map->numresources);

   if (!found)
       return giblorb_err_NotFound;

   glui32 chunknum = found->chunknum;
   if (chunknum >= map->numchunks)
       return giblorb_err_NotFound;

   giblorb_chunkdesc_t *chu = &(map->chunks[chunknum]);
   if (chu->auxdatnum < 0)
       return giblorb_err_NotFound;

   giblorb_auxpict_t *auxpict = &(map->auxpict[chu->auxdatnum]);
   if (!auxpict->loaded) {
       giblorb_result_t res;
       giblorb_err_t err = giblorb_load_chunk_by_number(map, giblorb_method_Memory, &res, chunknum);
       if (err)
           return err;

       if (chu->type == giblorb_ID_JPEG)
           err = giblorb_image_get_size_jpeg(res.data.ptr, res.length, auxpict);
       else if (chu->type == giblorb_ID_PNG)
           err = giblorb_image_get_size_png(res.data.ptr, res.length, auxpict);
       else
           err = giblorb_err_Format;

       giblorb_unload_chunk(map, chunknum);

       if (err)
           return err;

       auxpict->loaded = TRUE;
   }

   res->chunktype = chu->type;
   res->width = auxpict->width;
   res->height = auxpict->height;
   res->alttext = auxpict->alttext;
   return giblorb_err_None;
}

static giblorb_err_t giblorb_image_get_size_jpeg(unsigned char *arr, glui32 length, giblorb_auxpict_t *auxpict)
{
   int pos = 0;
   while (pos < length) {
       if (arr[pos] != 0xFF) {
           /* error: find_dimensions_jpeg: marker is not 0xFF */
           return giblorb_err_Format;
       }
       while (arr[pos] == 0xFF)
           pos += 1;
       unsigned char marker = arr[pos];
       pos += 1;
       if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD9)) {
           /* marker type has no data */
           continue;
       }
       int chunklen = (arr[pos+0] << 8) | (arr[pos+1]);
       if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC8) {
           if (chunklen < 7) {
               /* error: find_dimensions_jpeg: SOF block is too small */
               return giblorb_err_Format;
           }
           auxpict->height = (arr[pos+3] << 8) | (arr[pos+4]);
           auxpict->width  = (arr[pos+5] << 8) | (arr[pos+6]);
           return giblorb_err_None;
       }
       pos += chunklen;
   }

   /* error: find_dimensions_jpeg: no SOF marker found */
   return giblorb_err_Format;
}

static giblorb_err_t giblorb_image_get_size_png(unsigned char *arr, glui32 length, giblorb_auxpict_t *auxpict)
{
   int pos = 0;
   if (length < 8)
       return giblorb_err_Format;
   if (arr[0] != 0x89 || arr[1] != 'P' || arr[2] != 'N' || arr[3] != 'G') {
       /* error: find_dimensions_png: PNG signature does not match */
       return giblorb_err_Format;
   }
   pos += 8;
   while (pos < length) {
       glui32 chunklen = giblorb_native4(arr+pos);
       pos += 4;
       glui32 chunktype = giblorb_native4(arr+pos);
       pos += 4;
       if (chunktype == giblorb_make_id('I', 'H', 'D', 'R')) {
           auxpict->width = giblorb_native4(arr+pos);
           pos += 4;
           auxpict->height = giblorb_native4(arr+pos);
           pos += 4;
           return giblorb_err_None;
       }
       pos += chunklen;
       pos += 4; /* skip CRC */
   }

   /* error: find_dimensions_png: no PNG header block found */
   return giblorb_err_Format;
}

/* 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;
   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);
}