/*------------------------------------------------------------------------------
 * Reads regular Amiga IFF ILBM files.
 *
 * Supports IMLIB2_LBM_NOMASK environment variable. If this is set to "1", then
 * a transparency mask in an image will be ignored. On the Amiga a mask is often
 * applied only when loading a brush rather than a picture, but this loader has
 * no way to tell when the user wants this behaviour from the picture alone.
 *
 * Author:  John Bickers <jbickers@ihug.co.nz>
 * Since:   2004-08-21
 * Version: 2004-08-28
 *------------------------------------------------------------------------------*/

#include "loader_common.h"

#define DBG_PFX "LDR-lbm"

#define L2RLONG(a) ((((int)((a)[0]) & 0xff) << 24) + (((int)((a)[1]) & 0xff) << 16) + (((int)((a)[2]) & 0xff) << 8) + ((int)((a)[3]) & 0xff))
#define L2RWORD(a) ((((int)((a)[0]) & 0xff) << 8) + ((int)((a)[1]) & 0xff))

typedef struct {
   int                 size;
   const unsigned char *data;
} CHUNK;

typedef struct {
   CHUNK               bmhd;
   CHUNK               camg;
   CHUNK               cmap;
   CHUNK               ctbl;
   CHUNK               sham;
   CHUNK               body;
   unsigned char      *cmap_alloc;      /* Modified colormap */

   int                 depth;
   int                 mask;
   int                 ham;
   int                 hbrite;

   int                 row;

   int                 offset;
   int                 count;
   int                 rle;
} ILBM;

/*------------------------------------------------------------------------------
 * Frees memory allocated as part of an ILBM structure.
 *------------------------------------------------------------------------------*/
static void
freeilbm(ILBM * ilbm)
{
   free(ilbm->cmap_alloc);
}

#define mm_check(p) ((const unsigned char *)(p) <= fptr + size)

/*------------------------------------------------------------------------------
 * Reads the given chunks out of a file, returns 0 if the file had a problem.
 *
 * Format FORMsizeILBMtag.size....tag.size....tag.size....
 *------------------------------------------------------------------------------*/
static int
loadchunks(const unsigned char *fptr, unsigned int size, ILBM * ilbm, int full)
{
   CHUNK              *c;
   int                 formsize, z;
   int                 ok;
   const unsigned char *buf;

   ok = 0;

   buf = fptr;
   if (!mm_check(buf + 12))
      return ok;

   if (memcmp(buf, "FORM", 4) != 0 || memcmp(buf + 8, "ILBM", 4) != 0)
      return ok;

   formsize = L2RLONG(buf + 4);

   D("%s: %.4s %.4s formsize=%d\n", __func__, buf, buf + 8, formsize);

   buf += 12;

   while (1)
     {
        if (buf >= fptr + formsize + 8)
           break;
        if (!mm_check(buf + 8))
           break;               /* Error or short file. */

        z = L2RLONG(buf + 4);
        if (z < 0)
           break;               /* Corrupt file. */

        D("%s: %.4s %d\n", __func__, buf, z);

        c = NULL;
        if (!memcmp(buf, "BMHD", 4))
           c = &(ilbm->bmhd);
        else if (full)
          {
             if (!memcmp(buf, "CAMG", 4))
                c = &(ilbm->camg);
             else if (!memcmp(buf, "CMAP", 4))
                c = &(ilbm->cmap);
             else if (!memcmp(buf, "CTBL", 4))
                c = &(ilbm->ctbl);
             else if (!memcmp(buf, "SHAM", 4))
                c = &(ilbm->sham);
             else if (!memcmp(buf, "BODY", 4))
                c = &(ilbm->body);
          }

        buf += 8;

        if (c && !c->data)
          {
             c->size = z;
             if (!mm_check(buf + c->size))
                break;
             c->data = buf;

             if (!full)
               {                /* Only BMHD required. */
                  ok = 1;
                  break;
               }
          }

        buf += z;
     }

   /* File may end strangely, especially if body size is uneven, but it's
    * ok if we have the chunks we want. !full check is already done. */
   if (ilbm->bmhd.data && ilbm->body.data)
      ok = 1;

   return ok;
}

/*------------------------------------------------------------------------------
 * Unpacks a row of possibly RLE data at a time.
 *
 * RLE compression depends on a count byte, followed by data bytes.
 *
 * 0x80 means skip.
 * 0xff to 0x81 means repeat one data byte (256 - count) + 1 times.
 * 0x00 to 0x7f means copy count + 1 data bytes.
 *
 * In theory RLE compression is not supposed to create runs across scanlines.
 *------------------------------------------------------------------------------*/
static void
bodyrow(unsigned char *p, int z, ILBM * ilbm)
{
   int                 i, x, w;
   unsigned char       b;

   if (ilbm->offset >= ilbm->body.size)
     {
        memset(p, 0, z);
        return;
     }

   if (!ilbm->rle)
     {
        w = ilbm->body.size - ilbm->offset;
        if (w > z)
           w = z;
        memcpy(p, ilbm->body.data + ilbm->offset, w);
        if (w < z)
           memset(p + w, 0, z - w);
        ilbm->offset += w;
        return;
     }

#ifdef __clang_analyzer__
   memset(p, 0, z);
#endif

   for (i = 0; i < z; i += w)
     {
        if (ilbm->offset < ilbm->body.size)
          {
             b = ilbm->body.data[ilbm->offset++];
             while (b == 0x80 && ilbm->offset < ilbm->body.size)
                b = ilbm->body.data[ilbm->offset++];
          }

        if (ilbm->offset >= ilbm->body.size)
          {
             w = z - i;
             memset(p + i, 0, w);
          }
        else if (b & 0x80)
          {
             w = (0x100 - b) + 1;
             if (w > z - i)
                w = z - i;

             b = ilbm->body.data[ilbm->offset++];
             memset(p + i, b, w);
          }
        else
          {
             w = (b & 0x7f) + 1;
             if (w > ilbm->body.size - ilbm->offset)
                w = ilbm->body.size - ilbm->offset;
             x = (w <= z - i) ? w : z - i;
             memcpy(p + i, ilbm->body.data + ilbm->offset, x);
             ilbm->offset += w;
             w = x;
          }
     }
}

/*------------------------------------------------------------------------------
 * Shifts a value to produce an 8-bit colour gun, and fills in the lower bits
 * from the high bits of the value so that, for example, 4-bit 0x0f scales to
 * 0xff, or 1-bit 0x01 scales to 0xff.
 *------------------------------------------------------------------------------*/
static unsigned char
scalegun(unsigned char v, int sl)
{
   int                 sr;

   switch (sl)
     {
     case 1:
     case 2:
     case 3:
        sr = 8 - sl;
        return (v << sl) | (v >> sr);

     case 4:
        return (v << 4) | v;

     case 5:
        return v * 0x24;

     case 6:
        return v * 0x55;

     case 7:
        return v * 0xff;
     }
   return v;
}

/*------------------------------------------------------------------------------
 * Scales the colours in a CMAP chunk if they all look like 4-bit colour, so
 * that they use all 8-bits. This is done by copying the high nybble into the
 * low nybble, so for example 0xf0 becomes 0xff.
 *------------------------------------------------------------------------------*/
static void
scalecmap(ILBM * ilbm)
{
   int                 i;

   if (!ilbm->cmap.data || ilbm->cmap.size <= 0)
      return;

   for (i = 0; i < ilbm->cmap.size; i++)
      if (ilbm->cmap.data[i] & 0x0f)
         return;

   ilbm->cmap_alloc = malloc(ilbm->cmap.size);

   for (i = 0; i < ilbm->cmap.size; i++)
      ilbm->cmap_alloc[i] = ilbm->cmap.data[i] | ilbm->cmap.data[i] >> 4;

   ilbm->cmap.data = ilbm->cmap_alloc;
}

/*------------------------------------------------------------------------------
 * Deplanes and converts an array of bitplanes to a single scanline of uint32_t
 * (unsigned int) values. uint32_t is ARGB.
 *------------------------------------------------------------------------------*/
static void
deplane(uint32_t * row, int w, ILBM * ilbm, unsigned char *plane[])
{
   unsigned int        l, r, g, b, a;
   int                 i, o, x;
   unsigned char       bit, v, h;
   const unsigned char *pal;

   pal = NULL;
   if (ilbm->sham.data && ilbm->sham.size >= 2 + (ilbm->row + 1) * 2 * 16)
      pal = ilbm->sham.data + 2 + ilbm->row * 2 * 16;
   if (ilbm->ctbl.data && ilbm->ctbl.size >= (ilbm->row + 1) * 2 * 16)
      pal = ilbm->ctbl.data + ilbm->row * 2 * 16;

   if (ilbm->ham)
      r = g = b = 0;

   bit = 0x80;
   o = 0;
   for (x = 0; x < w; x++)
     {
        l = 0;
        for (i = ilbm->depth - 1; i >= 0; i--)
          {
             l = l << 1;
             if (plane[i][o] & bit)
                l = l | 1;
          }
        a = (ilbm->mask == 0
             || (ilbm->mask == 1 && (plane[ilbm->depth][o] & bit))
             || ilbm->mask == 2) ? 0xff : 0x00;

        if (ilbm->depth == 32)
          {
             a = (l >> 24) & 0xff;
             b = (l >> 16) & 0xff;
             g = (l >> 8) & 0xff;
             r = l & 0xff;
          }
        else if (ilbm->depth == 24)
          {
             b = (l >> 16) & 0xff;
             g = (l >> 8) & 0xff;
             r = l & 0xff;
          }
        else if (ilbm->ham)
          {
             v = l & ((1 << (ilbm->depth - 2)) - 1);
             h = (l & ~v) >> (ilbm->depth - 2);

             if (h == 0x00)
               {
                  if (!pal)
                    {
                       if ((v + 1) * 3 <= ilbm->cmap.size)
                         {
                            r = ilbm->cmap.data[v * 3];
                            g = ilbm->cmap.data[v * 3 + 1];
                            b = ilbm->cmap.data[v * 3 + 2];
                         }
                       else
                          r = g = b = 0;
                    }
                  else
                    {
                       r = scalegun(pal[v * 2] & 0x0f, 4);
                       g = scalegun((pal[v * 2 + 1] & 0xf0) >> 4, 4);
                       b = scalegun((pal[v * 2 + 1] & 0x0f), 4);
                    }
               }
             else if (h == 0x01)
                b = scalegun(v, 8 - (ilbm->depth - 2));
             else if (h == 0x02)
                r = scalegun(v, 8 - (ilbm->depth - 2));
             else
                g = scalegun(v, 8 - (ilbm->depth - 2));
          }
        else if (ilbm->hbrite)
          {
             v = l & ((1 << (ilbm->depth - 1)) - 1);
             h = (l & ~v) >> (ilbm->depth - 1);

             if (!pal)
               {
                  if ((v + 1) * 3 <= ilbm->cmap.size)
                    {
                       r = ilbm->cmap.data[v * 3];
                       g = ilbm->cmap.data[v * 3 + 1];
                       b = ilbm->cmap.data[v * 3 + 2];
                    }
                  else
                     r = g = b = 0;
               }
             else
               {
                  r = scalegun(pal[v * 2] & 0x0f, 4);
                  g = scalegun((pal[v * 2 + 1] & 0xf0) >> 4, 4);
                  b = scalegun((pal[v * 2 + 1] & 0x0f), 4);
               }

             if (h)
               {
                  r = r >> 1;
                  g = g >> 1;
                  b = b >> 1;
               }

             if (ilbm->mask == 2 && v == L2RWORD(ilbm->bmhd.data + 12))
                a = 0x00;
          }
        else if (ilbm->cmap.size == 0 && !pal)
          {
             v = l & ((1 << ilbm->depth) - 1);
             r = scalegun(v, ilbm->depth);
             g = r;
             b = r;
          }
        else
          {
             v = l & 0xff;
             if (!pal)
               {
                  if ((v + 1) * 3 <= ilbm->cmap.size)
                    {
                       r = ilbm->cmap.data[v * 3];
                       g = ilbm->cmap.data[v * 3 + 1];
                       b = ilbm->cmap.data[v * 3 + 2];
                    }
                  else
                     r = g = b = 0;
               }
             else
               {
                  r = scalegun(pal[v * 2] & 0x0f, 4);
                  g = scalegun((pal[v * 2 + 1] & 0xf0) >> 4, 4);
                  b = scalegun((pal[v * 2 + 1] & 0x0f), 4);
               }

             if (ilbm->mask == 2 && v == L2RWORD(ilbm->bmhd.data + 12))
                a = 0x00;
          }

        row[x] = PIXEL_ARGB(a, r, g, b);

        bit = bit >> 1;
        if (bit == 0)
          {
             o++;
             bit = 0x80;
          }
     }
}

/*------------------------------------------------------------------------------
 * Loads an image. If load_data is non-zero then the file is fully loaded,
 * otherwise only the width and height are read.
 *
 * Imlib2 doesn't support reading comment chunks like ANNO.
 *------------------------------------------------------------------------------*/
int
load2(ImlibImage * im, int load_data)
{
   int                 rc;
   void               *fdata;
   char               *env;
   int                 i, n, y, z;
   unsigned char      *plane[40];
   ILBM                ilbm;

   rc = LOAD_FAIL;

   fdata = mmap(NULL, im->fsize, PROT_READ, MAP_SHARED, fileno(im->fp), 0);
   if (fdata == MAP_FAILED)
      return LOAD_BADFILE;

   plane[0] = NULL;
   memset(&ilbm, 0, sizeof(ilbm));

  /*----------
   * Load the chunk(s) we're interested in. If load_data is not true, then we only
   * want the image size and format.
   *----------*/
   if (!loadchunks(fdata, im->fsize, &ilbm, load_data))
      goto quit;

   rc = LOAD_BADIMAGE;          /* Format accepted */

  /*----------
   * Use and check header.
   *----------*/

   if (ilbm.bmhd.size < 20)
      goto quit;

   im->w = L2RWORD(ilbm.bmhd.data);
   im->h = L2RWORD(ilbm.bmhd.data + 2);
   if (!IMAGE_DIMENSIONS_OK(im->w, im->h))
      goto quit;

   ilbm.depth = ilbm.bmhd.data[8];
   if (ilbm.depth < 1
       || (ilbm.depth > 8 && ilbm.depth != 24 && ilbm.depth != 32))
      goto quit;                /* Only 1 to 8, 24, or 32 planes. */

   ilbm.rle = ilbm.bmhd.data[10];
   if (ilbm.rle < 0 || ilbm.rle > 1)
      goto quit;                /* Only NONE or RLE compression. */

   ilbm.mask = ilbm.bmhd.data[9];

   IM_FLAG_UPDATE(im, F_HAS_ALPHA, ilbm.mask || ilbm.depth == 32);

   env = getenv("IMLIB2_LBM_NOMASK");
   if (env
       && (!strcmp(env, "true") || !strcmp(env, "1") || !strcmp(env, "yes")
           || !strcmp(env, "on")))
      IM_FLAG_CLR(im, F_HAS_ALPHA);

   if (!load_data)
      QUIT_WITH_RC(LOAD_SUCCESS);

   ilbm.ham = 0;
   ilbm.hbrite = 0;
   if (ilbm.depth <= 8)
     {
        if (ilbm.camg.size == 4)
          {
             if (ilbm.camg.data[2] & 0x08)
                ilbm.ham = 1;
             if (ilbm.camg.data[3] & 0x80)
                ilbm.hbrite = 1;
          }
        else
          {                     /* Only guess at ham and hbrite if CMAP is present. */
             if (ilbm.depth == 6 && ilbm.cmap.size >= 3 * 16)
                ilbm.ham = 1;
             if (!ilbm.ham && ilbm.depth > 1
                 && ilbm.cmap.size == 3 * (1 << (ilbm.depth - 1)))
                ilbm.hbrite = 1;
          }
     }

  /*----------
   * The source data is planar. Each plane is an even number of bytes wide. If
   * masking type is 1, there is an extra plane that defines the mask. Scanlines
   * from each plane are interleaved, from top to bottom. The first plane is the
   * 0 bit.
   *----------*/

   if (!__imlib_AllocateData(im))
      QUIT_WITH_RC(LOAD_OOM);

   n = ilbm.depth;
   if (ilbm.mask == 1)
      n++;
   plane[0] = malloc(((im->w + 15) / 16) * 2 * n);
   if (!plane[0])
      QUIT_WITH_RC(LOAD_OOM);

   for (i = 1; i < n; i++)
      plane[i] = plane[i - 1] + ((im->w + 15) / 16) * 2;

   z = ((im->w + 15) / 16) * 2 * n;

   scalecmap(&ilbm);

   for (y = 0; y < im->h; y++)
     {
        bodyrow(plane[0], z, &ilbm);

        deplane(im->data + im->w * y, im->w, &ilbm, plane);
        ilbm.row++;

        if (im->lc && __imlib_LoadProgressRows(im, y, 1))
           QUIT_WITH_RC(LOAD_BREAK);
     }

   rc = LOAD_SUCCESS;

 quit:
  /*----------
   * We either had a successful decode, the user cancelled, or we couldn't get
   * the memory for im->data or plane[0].
   *----------*/
   free(plane[0]);

   freeilbm(&ilbm);

   munmap(fdata, im->fsize);

   return rc;
}

/*------------------------------------------------------------------------------
 * Perhaps save only in 32-bit format? The IFF ILBM format has pretty much gone
 * the way of the Amiga, who saves in this format any more?
 *------------------------------------------------------------------------------*/
#if 0
char
save(ImlibImage * im, ImlibProgressFunction progress, char progress_granularity)
{
   return 0;
}
#endif

void
formats(ImlibLoader * l)
{
   static const char  *const list_formats[] = { "iff", "ilbm", "lbm" };
   __imlib_LoaderSetFormats(l, list_formats, ARRAY_SIZE(list_formats));
}
