/*
======================================================================
load.c

Ernie Wright  05 Feb 2012

Functions for reading and "playing" Amiga ANIM-J animations created
and played originally by Eric Graham's pilbm, dilbm, and movie.

ReadMovie()    Read and parse the entire movie into memory, initialize
               a front and back buffer.
ApplyDelta()   Apply a delta to the back buffer and flip the resulting
               image to the front buffer.
GetRow24()     Read out a single scanline from the front buffer,
               converting it to 24-bit RGB.

This code is part of a program that extracts an image sequence from a
movie file.  The program calls ReadMovie(), then steps through the
sequence of deltas in the file's ANSQ chunk to build each frame.

The Amiga modes supported by the decoder are

   color map         1 to 8 planes
   gray level        1 to 8 planes
   Hold and Modify   5 to 8 planes
   Extra Halfbrite   6 planes

In practice, ANIM-J's are most likely to contain 6-plane HAM or, less
often, 6-plane EHB images or indexed images up to 5 planes.
====================================================================== */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "iff.h"


/*
======================================================================
unHAM()

Decodes one CAMG_HAM scanline from an Amiga ILBM image.  Called by
GetRow24().

   movie    pointer to a MovieInfo initialized by ReadMovie()

The red, green and blue scanline buffers in the MovieInfo are filled
with byte-per-pixel channel data.

HAM (hold-and-modify) images store pixel colors as codes which are
divided into a mode in the high two bits and data in the other bits:

   00  data bits are an index into the color map
   01  data bits are blue level
   10  data bits are red level
   11  data bits are green level

Unless a pixel is color-mapped, only one of its three levels is given
in its code.  The other two are assumed to be the same as those for
the pixel to its left.  If the pixel is the first one in a scanline,
the pixel to its left is assumed to be RGB(0, 0, 0).

The number of data bits is 4 for standard HAM and 6 for HAM8.  The
data bits are precision-extended when the levels are moved to the
8-bit RGB buffers--regardless of the number of bits, the maximum
level will translate to 255 at 8 bits of precision.

It is possible for the mode to be a single bit.  In this case the
low bit is explicit and the high bit is assumed to be 0, implying
that only the blue level can be modified.  This is very seldom, if
ever, used, but is supported here because the cost is negligible
(which is probably why the Amiga display hardware supported it).
====================================================================== */

static void unHAM( MovieInfo *movie )
{
   RGBTriple prev;
   int i, j, hbits, mbits, mask;


   prev.red = prev.green = prev.blue = 0;
   hbits = movie->bmhd.nPlanes > 6 ? 6 : 4;
   mbits = 8 - hbits;
   mask  = ( 1 << hbits ) - 1;

   for ( i = 0; i < movie->bmhd.w; i++ ) {
      j = movie->buf[ i ];
      switch ( j >> hbits ) {
         case 0:
            movie->rgb[ 0 ][ i ] = movie->cmap[ j & mask ].red;
            movie->rgb[ 1 ][ i ] = movie->cmap[ j & mask ].green;
            movie->rgb[ 2 ][ i ] = movie->cmap[ j & mask ].blue;
            break;

         case 1:
            movie->rgb[ 0 ][ i ] = prev.red;
            movie->rgb[ 1 ][ i ] = prev.green;
            movie->rgb[ 2 ][ i ] = ( j & mask ) << mbits;
            movie->rgb[ 2 ][ i ] |= movie->rgb[ 2 ][ i ] >> hbits;
            break;

         case 2:
            movie->rgb[ 0 ][ i ] = ( j & mask ) << mbits;
            movie->rgb[ 0 ][ i ] |= movie->rgb[ 0 ][ i ] >> hbits;
            movie->rgb[ 1 ][ i ] = prev.green;
            movie->rgb[ 2 ][ i ] = prev.blue;
            break;

         case 3:
            movie->rgb[ 0 ][ i ] = prev.red;
            movie->rgb[ 1 ][ i ] = ( j & mask ) << mbits;
            movie->rgb[ 1 ][ i ] |= movie->rgb[ 1 ][ i ] >> hbits;
            movie->rgb[ 2 ][ i ] = prev.blue;
            break;
      }
      prev.red   = movie->rgb[ 0 ][ i ];
      prev.green = movie->rgb[ 1 ][ i ];
      prev.blue  = movie->rgb[ 2 ][ i ];
   }
}


/*
======================================================================
GetRow24()

Convert one scanline from an Amiga ILBM image to 24-bit RGB.

   movie    pointer to a MovieInfo initialized by ReadMovie()

If successful, the MovieInfo red, green and blue scanline buffers are
filled with byte-per-pixel channel data and the function returns TRUE.
Otherwise it returns FALSE.

IFF ILBMs store images with 256 or fewer colors as arrays of color
map indexes.  The color map index for a given pixel is developed by
collecting each bit at a given position from each bitplane.  The
index is then used to look up the pixel's RGB levels in a table,
which is stored in ILBMs as a CMAP chunk.

The bit collection process can be regarded as a 90-degree bitmap
rotation.  A single ILBM scanline is treated as a bitmap with a height
of nPlanes and a width equal to the image width divided by 8.  This
function performs a 90-degree clockwise rotation on squares of bits
that are 8 bits wide and high, converting 8 pixels at a time.

If the image is HAM (hold-and-modify), the numbers in the index array
are actually codes that may contain either color map references or
RGB levels.  See the comments for the unHAM() function.  If the image
is EHB (extra-halfbrite), the color map in the file contains 32
entries, but the indexes range from 0 to 63.
====================================================================== */

void GetRow24( MovieInfo *movie, int y )
{
   int i, j, k;
   unsigned char *b;

   /* Copy the y-th scanline into the scanline buffer.  Bits in planes
      higher than nPlanes need to be 0. */

   b = movie->front + y * movie->rowsize * movie->bmhd.nPlanes;
   memcpy( movie->buf, b, movie->rowsize * movie->bmhd.nPlanes );
   if ( movie->bmhd.nPlanes < 8 ) {
      i = ( 8 - movie->bmhd.nPlanes ) * movie->rowsize;
      b = movie->buf + movie->bmhd.nPlanes * movie->rowsize;
      memset( b, 0, i );
   }

   /* Collect bits from nPlanes bitplanes into a single byte.  Bits
      form indexes into the colormap (or HAM or EHB codes), which
      we'll store temporarily in the blue buffer and copy back to
      the buf buffer. */

   k = 0;
   b = movie->buf;
   for ( j = 0; j < movie->rowsize; j++ ) {
      bitrot_cw( b, movie->rowsize, movie->rgb[ 2 ] + k, 1 );
      k += 8;
      ++b;
   }
   memcpy( movie->buf, movie->rgb[ 2 ], movie->bmhd.w );

   /* convert from indexes in buf to 24-bit RGB */

   if ( movie->camg & CAMG_HAM )
      unHAM( movie );
   else
      /* fill rgb buffers with values from the colormap */
      for ( j = 0; j < movie->bmhd.w; j++ ) {
         k = movie->buf[ j ];
         movie->rgb[ 0 ][ j ] = movie->cmap[ k ].red;
         movie->rgb[ 1 ][ j ] = movie->cmap[ k ].green;
         movie->rgb[ 2 ][ j ] = movie->cmap[ k ].blue;
      }

   return TRUE;
}


/*
======================================================================
ReadILBM()

Read the first frame of a movie.  Called by ReadMovie().

   movie       the MovieInfo to initialize

If successful, this function reads the first FORM ILBM from an ANIM-J
file.  This ILBM is the first frame of the movie, and except for being
embedded in an ANIM file, its structure is identical to a freestanding
IFF ILBM image.  It defines the size, bit depth, pixel interpretation,
and color table for the entire movie.  The function reads, but doesn't
decompress, the BODY of the ILBM.  If it fails, it returns one of the
error codes defined in iff.h.
====================================================================== */

static int ReadILBM( MovieInfo *movie )
{
   long ck[ 3 ], form_end, pos;
   int i, result;

   /* read the FORM header */

   result = EPNOILBM;
   fread( ck, 12, 1, movie->fp );

   /* FORM ILBM? */

   if ( ck[ 0 ] != ID_FORM || ck[ 2 ] != ID_ILBM ) return result;

   /* remember the FORM size */

   ck[ 2 ] = ck[ 1 ];
   revbytes( &ck[ 2 ], 4, 1 );
   pos = ftell( movie->fp );
   form_end = pos + ck[ 2 ] - 4;

   /* get BMHD, CAMG, CMAP and stop on BODY */

   fread( ck, 8, 1, movie->fp );
   revbytes( &ck[ 1 ], 4, 1 );

   while ( 1 ) {
      ck[ 1 ] += ( ck[ 1 ] & 1 );
      if ( ck[ 0 ] == ID_BODY ) break;

      if ( ck[ 0 ] == ID_BMHD ) {
         fread( &movie->bmhd, ck[ 1 ], 1, movie->fp );
         revbytes( &movie->bmhd.w, 2, 4 );
         revbytes( &movie->bmhd.transparentColor, 2, 1 );
         revbytes( &movie->bmhd.pw, 2, 2 );
      }

      else if ( ck[ 0 ] == ID_CMAP ) {
         result = EPNOMEM;
         movie->cmap = malloc( ck[ 1 ] );
         if ( !movie->cmap ) return result;
         movie->ncolors = ck[ 1 ] / 3;
         fread( movie->cmap, ck[ 1 ], 1, movie->fp );
      }

      else if ( ck[ 0 ] == ID_CAMG ) {
         fread( &movie->camg, ck[ 1 ], 1, movie->fp );
         revbytes( &movie->camg, ck[ 1 ], 1 );
      }

      else
         fseek( movie->fp, ck[ 1 ], SEEK_CUR );

      if ( form_end <= ftell( movie->fp ))   /* past the end of the FORM? */
         break;

      fread( ck, 8, 1, movie->fp );
      revbytes( &ck[ 1 ], 4, 1 );
   }

   /* found a BODY? */

   result = EPNOBODY;
   if ( ck[ 0 ] != ID_BODY ) return result;

   /* found a BMHD? */

   result = EPNOBMHD;
   if ( movie->bmhd.w == 0 ) return result;

   /* number of planes makes sense? */

   result = EPPLANES;
   if ( movie->bmhd.nPlanes > 8 )
      return result;

   /* if no CMAP, assume gray and create our own color table */

   if ( movie->bmhd.nPlanes <= 8 && !movie->cmap ) {
      movie->isgray = TRUE;
      movie->ncolors = 1 << movie->bmhd.nPlanes;
      result = EPNOMEM;
      movie->cmap = calloc( movie->ncolors, sizeof( RGBTriple ));
      if ( !movie->cmap ) return result;
      for ( i = 0; i < movie->ncolors; i++ )
         movie->cmap[ i ].red = movie->cmap[ i ].green = movie->cmap[ i ].blue
            = i * 255 / movie->ncolors;
   }

   /* if EHB, extend the color table */

   if ( movie->camg & CAMG_EHB ) {
      result = EPNOMEM;
      movie->cmap = realloc( movie->cmap, movie->ncolors * 6 );
      if ( !movie->cmap ) return result;
      for ( i = movie->ncolors; i < movie->ncolors * 2; i++ ) {
         movie->cmap[ i ].red   = movie->cmap[ i - movie->ncolors ].red   >> 1;
         movie->cmap[ i ].green = movie->cmap[ i - movie->ncolors ].green >> 1;
         movie->cmap[ i ].blue  = movie->cmap[ i - movie->ncolors ].blue  >> 1;
      }
      movie->ncolors *= 2;
   }

   /* allocate scanline buffers and body */

   result = EPNOMEM;
   movie->rowsize = (( movie->bmhd.w + 15 ) >> 3 ) & 0xFFFE;
   movie->buf = calloc( movie->rowsize, 32 );
   movie->body = movie->bp = malloc( ck[ 1 ] );
   if ( !movie->buf || !movie->body ) return result;

   movie->rgb[ 0 ] = movie->buf + movie->rowsize * 8;
   movie->rgb[ 1 ] = movie->buf + movie->rowsize * 16;
   movie->rgb[ 2 ] = movie->buf + movie->rowsize * 24;

   /* read the body */

   fread( movie->body, ck[ 1 ], 1, movie->fp );

   /* successful */

   return 0;
}


/*
======================================================================
count_deltas()

Count the deltas in an ANIM-J file.  ReadMovie() calls this after
loading the first frame, and here we assume that the first frame is
immediately followed by some number of FORM ILBM chunks, each of which
contain an ANHD and a DLTA.  The function returns as soon as it finds
a chunk that doesn't follow this pattern (which *should* be when it
finds the ANSQ chunk).  The file pointer is reset to the start of the
first FORM ILBM ANHD.
====================================================================== */

static int count_deltas( MovieInfo *movie )
{
   long ck[ 4 ], pos;
   int n;

   /* remember where we started */
   pos = ftell( movie->fp );
   
   /* read the first chunk header */
   fread( ck, 16, 1, movie->fp );
   revbytes( &ck[ 1 ], 4, 1 );

   for ( n = 0;; n++ ) {
      /* next chunk starts on an even byte boundary */
      ck[ 1 ] += ( ck[ 1 ] & 1 );
      
      /* if the chunk's not an ANHD, we're done */
      if ( ck[ 0 ] != ID_FORM || ck[ 2 ] != ID_ILBM || ck[ 3 ] != ID_ANHD )
         break;

      /* read the next chunk header */
      fseek( movie->fp, ck[ 1 ] - 8, SEEK_CUR );
      fread( ck, 16, 1, movie->fp );
      if ( feof( movie->fp )) return 0;
      revbytes( &ck[ 1 ], 4, 1 );
   }

   /* go back to where we started */
   fseek( movie->fp, pos, SEEK_SET );

   /* return the number of ANHDs */
   return n;
}


/*
======================================================================
ReadDLTA()

Read and store the contents of the next DLTA chunk in an ANIM-J.  Each
DLTA is preceeded by an ANHD, both of which are wrapped in a FORM
ILBM.  ANIM-J movies don't store anything useful in the ANHD, but we
verify that the compression type, the first byte, is 74 (ASCII 'J').
Returns 1 if successful, otherwise 0.
====================================================================== */

static int ReadDLTA( MovieInfo *movie, int i )
{
   long ck[ 3 ];
   unsigned char byte;

   fread( ck, 12, 1, movie->fp );
   revbytes( &ck[ 1 ], 4, 1 );

   /* begins with FORM ILBM? */
   if ( ck[ 0 ] != ID_FORM || ck[ 2 ] != ID_ILBM ) return 0;

   /* ANHD chunk next? */
   fread( ck, 8, 1, movie->fp );
   revbytes( &ck[ 1 ], 4, 1 );
   if ( ck[ 0 ] != ID_ANHD ) return 0;
   
   /* Is this an ANIM-J delta? */
   fread( &byte, 1, 1, movie->fp );
   if ( byte != 'J' ) return 0;
   
   /* skip the rest of the ANHD */
   fseek( movie->fp, ck[ 1 ] - 1, SEEK_CUR );

   /* DLTA chunk next? */
   fread( ck, 8, 1, movie->fp );
   revbytes( &ck[ 1 ], 4, 1 );
   if ( ck[ 0 ] != ID_DLTA ) return 0;
   
   /* read and store the DLTA bytes */
   movie->delta[ i ].size = ck[ 1 ];
   ck[ 1 ] += ( ck[ 1 ] & 1 );

   movie->delta[ i ].bytes = calloc( 1, ck[ 1 ] );
   if ( !movie->delta[ i ].bytes ) return 0;
   fread( movie->delta[ i ].bytes, 1, ck[ 1 ], movie->fp );
   return 1;
}


/*
======================================================================
get_row()

Decode one scanline from BODY into the frame buffer.  The source is
movie->bp and the destination is movie->fb.  Both are updated here, so
they only need to be initialized for the first scanline.
====================================================================== */

static int get_row( MovieInfo *movie )
{
   int p;

   switch ( movie->bmhd.compression ) {
      case cmpByteRun1:
         for ( p = 0; p < movie->bmhd.nPlanes; p++ ) {
            if ( !unpack( &movie->bp, movie->fb, movie->rowsize )) return FALSE;
            movie->fb += movie->rowsize;
         }
         if ( movie->bmhd.masking == mskHasMask )
            if ( !unpack_ro( &movie->bp, movie->rowsize )) return FALSE;
         break;

      case cmpNone:
         memcpy( movie->fb, movie->bp, movie->rowsize * movie->bmhd.nPlanes );
         movie->bp += movie->rowsize * movie->bmhd.nPlanes;
         movie->fb += movie->rowsize * movie->bmhd.nPlanes;
         if ( movie->bmhd.masking == mskHasMask )
            movie->bp += movie->rowsize;
         break;

      default:
         return FALSE;
   }

   return TRUE;
}


/*
======================================================================
init_frame_buffers()

Initialize the front and back image buffers by filling both of them
with the first frame of the animation.
====================================================================== */

static int init_frame_buffers( MovieInfo *movie )
{
   int framesize, i;

   framesize = movie->bmhd.h * movie->rowsize * movie->bmhd.nPlanes;
   movie->front = calloc( 1, framesize );
   movie->back = calloc( 1, framesize );
   if ( !movie->front || !movie->back ) return 0;
   movie->fb = movie->front;
   movie->bp = movie->body;
   for ( i = 0; i < movie->bmhd.h; i++ )
      if ( !get_row( movie )) return 0;
   memcpy( movie->back, movie->front, framesize );
   return 1;
}


/*
======================================================================
ReadMovie()

Read an ANIM-J animation and create a MovieInfo describing it.

   filename    the name of an IFF ANIM-J file
   result      pointer to storage for an error code

If successful, this function returns a pointer to a full MovieInfo,
basically a memory copy of the animation, and the result value will be
0.  Otherwise NULL is returned and result is set to an error code.

Possible errors include

   Memory allocation failed
   File open, read or seek failed
   File is a mangled IFF
   File isn't an IFF
   File isn't an ANIM-J
   Image isn't a recognized depth (1-8)
   BMHD chunk not found
   BODY chunk not found
====================================================================== */

MovieInfo *ReadMovie( const char *filename, int *result )
{
   long ck[ 3 ];
   int i;
   MovieInfo *movie;

   /* allocate a MovieInfo */

   *result = EPNOMEM;
   movie = ( MovieInfo * ) calloc( sizeof( MovieInfo ), 1 );
   if ( !movie ) return NULL;

   /* cache the filename */

   movie->filename = malloc( strlen( filename ) + 1 );
   if ( !movie->filename ) return FreeMovie( movie );
   strcpy( movie->filename, filename );

   /* open the image file */

   *result = EPNOFILE;
   movie->fp = fopen( filename, "rb" );
   if ( !movie->fp ) return FreeMovie( movie );

   /* read the FORM header */

   *result = EPNOANIM;
   fread( ck, 12, 1, movie->fp );

   /* FORM ANIM? */

   if ( ck[ 0 ] != ID_FORM || ck[ 2 ] != ID_ANIM ) return FreeMovie( movie );

   /* remember the FORM size */

   ck[ 2 ] = ck[ 1 ];
   revbytes( &ck[ 2 ], 4, 1 );

   /* read the first frame */

   *result = ReadILBM( movie );
   if ( *result ) return FreeMovie( movie );

   /* count the DLTAs */

   *result = EPDLTACT;
   movie->ndeltas = count_deltas( movie );
   if ( !movie->ndeltas ) return FreeMovie( movie );

   /* allocate and load the delta array */

   *result = EPNOMEM;
   movie->delta = calloc( movie->ndeltas, sizeof( DLTA ));
   if ( !movie->delta ) return FreeMovie( movie );

   *result = EPDLTARD;
   for ( i = 0; i < movie->ndeltas; i++ )
      if ( !ReadDLTA( movie, i )) return FreeMovie( movie );

   /* read the ANSQ */

   *result = EPNOANSQ;
   fread( ck, 8, 1, movie->fp );
   revbytes( &ck[ 1 ], 4, 1 );
   if ( ck[ 0 ] != ID_ANSQ ) return FreeMovie( movie );

   *result = EPNOMEM;
   movie->nansqs = ck[ 1 ] / 4;
   movie->ansq = calloc( movie->nansqs, sizeof( ANSQ ));
   if ( !movie->ansq ) return FreeMovie( movie );

   fread( movie->ansq, sizeof( ANSQ ), movie->nansqs, movie->fp );
   revbytes( movie->ansq, 2, movie->nansqs * 2 );

   /* done with the file */

   fclose( movie->fp );
   movie->fp = NULL;

   /* decode the BODY into the frame buffers */

   *result = EPFRAME1;
   if ( !init_frame_buffers( movie )) return FreeMovie( movie );

   /* successful */

   *result = 0;
   return movie;
}


/*
======================================================================
mem_short()

Extract a big-endian short int from memory.
====================================================================== */

static int mem_short( unsigned char *p )
{
   return p[ 0 ] << 8 | p[ 1 ];
}


/*
======================================================================
ApplyDelta()

Generate the next frame of the movie by applying the appropriate delta
to the back buffer and then flipping the back to the front.

   movie    the movie you're currently "playing"
   i        index of the delta to be applied

The delta index will usually come from the ANSQ (animation sequence)
array in the file.

The delta decoding is based on unmovie.c by Steven Den Beste, who
reverse-engineered the otherwise undocumented encoding.
====================================================================== */

int ApplyDelta( MovieInfo *movie, int i )
{
   unsigned char *dp, *dst, byte;
   int type, dir, height, width, nblocks, offset, b, x, y, z, done = 0;

   dp = movie->delta[ i ].bytes;

   while ( dp < movie->delta[ i ].bytes + movie->delta[ i ].size ) {
      type = mem_short( dp );
      dp += 2;

      switch ( type ) {
         case 0:
            done = 1;
            break;
         case 1:
            dir     = mem_short( dp );
            height  = mem_short( dp + 2 );
            nblocks = mem_short( dp + 4 );
            width   = 1;
            dp += 6;
            break;
         case 2:
            dir     = mem_short( dp );
            height  = mem_short( dp + 2 );
            width   = mem_short( dp + 4 );
            nblocks = mem_short( dp + 6 );
            dp += 8;
            break;
         default:
            return 0;
      }
      if ( done ) break;

      for ( b = 0; b < nblocks; b++ ) {
         offset = mem_short( dp );
         dp += 2;
         /* convert from contiguous to interleaved */
         y = offset / movie->rowsize;
         x = offset % movie->rowsize;
         dst = movie->back + y * movie->rowsize * movie->bmhd.nPlanes + x;

         for ( y = 0; y < height; y++ ) {
            for ( z = 0; z < movie->bmhd.nPlanes; z++ ) {
               for ( x = 0; x < width; x++ ) {
                  byte = *dp++;
                  if ( dir )
                     dst[ x ] ^= byte;
                  else
                     dst[ x ] = byte;
               }
               dst += movie->rowsize;
            }
         }
      }

      /* skip odd byte */
      if (( nblocks * height * width * movie->bmhd.nPlanes ) & 1 ) ++dp;
   }

   /* swap the frame buffers */
   dst = movie->front;
   movie->front = movie->back;
   movie->back = dst;

   return 1;
}
