/* pamflip.c - perform one or more flip operations on a Netpbm image
**
** Copyright (C) 1989 by Jef Poskanzer.
**
** Permission to use, copy, modify, and distribute this software and its
** documentation for any purpose and without fee is hereby granted, provided
** that the above copyright notice appear in all copies and that both that
** copyright notice and this permission notice appear in supporting
** documentation.  This software is provided "as is" without express or
** implied warranty.
*/

/*
   transformNonPbmChunk() is the general transformation function.
   It can tranform anything, albeit slowly and expensively.
   
   The following are enhancements for specific cases:
   
     transformRowByRowPbm():
       PBM image with left-right or null transformation
     transformRowsBottomTopPbm()
       PBM image with bottom-top transformation
     transformRowByRowNonPbm()
       non-PBM image with left-right or null transformation
       (also works for PBM, but we don't use it)
     transformRowsBottomTopNonPbm()
       non-PBM image with bottom-top transformation
       (also works for PBM, but we don't use it)
     transformPbm()
       PBM image with column-for-row transformation
       (also works for all other transformations, but we don't use it)
     transformNonPbmWhole()
       non-PBM column-for-row transformation
       (also works for PBM, but we don't use it)

   Except we don't use any of the above if we can't fit the entire image
   into the amount of memory the user gave us to work with.  In that
   case, we fall back to transformNonPbmChunk().

   Note that while virtual memory can be limited (e.g. sometimes the
   machine's 32 bit address space itself is a limit), real memory is
   also a concern.  With a row-for-column transformation
   (e.g. -transpose), if we can't fit the entire image into _real_
   memory, we will page thrash within an inch our lives.  So we count
   on the user to tell us how much real memory we should expect to
   get -- his -memsize option is the lesser of the available virtual
   and real memory.

   Before Netpbm 10.42 (March 2008), we had a different method for
   dealing with memory shortage.  We didn't worry about virtual memory
   at all, and always kept the whole target image in memory.  We did
   not use temporary files.  But to avoid page thrashing in a
   column-for-row transformation, we transformed in column stripes of
   the source image, reading the input image through multiple times.
*/

#define _BSD_SOURCE 1      /* Make sure strdup() is in string.h */
#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */

#include <assert.h>
#include <limits.h>
#include <string.h>

#include "pm_c_util.h"
#include "pam.h"
#include "shhopt.h"
#include "mallocvar.h"
#include "nstring.h"
#include "bitreverse.h"

enum xformType {LEFTRIGHT, TOPBOTTOM, TRANSPOSE};

static void
parseXformOpt(const char *     const xformOpt,
              unsigned int  *  const xformCountP,
              enum xformType * const xformList) {
/*----------------------------------------------------------------------------
   Translate the -xform option string into an array of transform types.

   Return the array as xformList[], which is preallocated for at least
   10 elements.
-----------------------------------------------------------------------------*/
    unsigned int xformCount;
    char * xformOptWork;
    char * cursor;
    bool eol;
    
    xformOptWork = strdup(xformOpt);
    cursor = &xformOptWork[0];
    
    eol = FALSE;    /* initial value */
    xformCount = 0; /* initial value */
    while (!eol && xformCount < 10) {
        const char * token;
        token = strsepN(&cursor, ",");
        if (token) {
            if (streq(token, "leftright"))
                xformList[xformCount++] = LEFTRIGHT;
            else if (streq(token, "topbottom"))
                xformList[xformCount++] = TOPBOTTOM;
            else if (streq(token, "transpose"))
                xformList[xformCount++] = TRANSPOSE;
            else if (streq(token, ""))
            { /* ignore it */}
            else
                pm_error("Invalid transform type in -xform option: '%s'",
                         token );
        } else
            eol = TRUE;
    }
    free(xformOptWork);

    *xformCountP = xformCount;
}



/* See transformPoint() for an explanation of the transform matrix types.
   The difference between the two types is that 'xformCore' is particular
   to the source image dimensions and can be used to do the transformation,
   while 'xformCore' is independent of the source image and just
   tells what kind of transformation.
*/

struct xformCore {
    /* a b
       c d
    */
    int a;  /* -1, 0, or 1 */
    int b;  /* -1, 0, or 1 */
    int c;  /* -1, 0, or 1 */
    int d;  /* -1, 0, or 1 */
};



struct xformMatrix {
    /* a b 0
       c d 0
       e f 1
    */
    int a;  /* -1, 0, or 1 */
    int b;  /* -1, 0, or 1 */
    int c;  /* -1, 0, or 1 */
    int d;  /* -1, 0, or 1 */
    int e;  /* 0 or maximum column number in target image */
    int f;  /* 0 or maximum row number in target image */
};



static void
leftright(struct xformCore * const xformP) {
    xformP->a = - xformP->a;
    xformP->c = - xformP->c;
}



static void
topbottom(struct xformCore * const xformP) {
    xformP->b = - xformP->b;
    xformP->d = - xformP->d;
}



static void
swap(int * const xP, int * const yP) {

    int const t = *xP;

    *xP = *yP;
    *yP = t;
}



static void
transpose(struct xformCore * const xformP) {
    swap(&xformP->a, &xformP->b);
    swap(&xformP->c, &xformP->d);
}



static void
computeXformCore(unsigned int       const xformCount,
                 enum xformType     const xformType[],
                 struct xformCore * const xformP) {
    
    struct xformCore const nullTransform = {1, 0, 0, 1};

    unsigned int i;

    *xformP = nullTransform;   /* initial value */

    for (i = 0; i < xformCount; ++i) {
        switch (xformType[i]) {
        case LEFTRIGHT: 
            leftright(xformP);
            break;
        case TOPBOTTOM:
            topbottom(xformP);
            break;
        case TRANSPOSE:
            transpose(xformP);
            break;
        }
    }
}



static void
xformDimensions(struct xformCore const xform,
                unsigned int     const inCols,
                unsigned int     const inRows,
                unsigned int *   const outColsP,
                unsigned int *   const outRowsP) {
/*----------------------------------------------------------------------------
   Compute the output dimensions from the input dimensions given a
   matrix that describes a transformation.

   E.g. if it's a 90 degree rotation of a 10 x 20 image, the output is
   a 20 x 10 image.
-----------------------------------------------------------------------------*/
    *outColsP = abs(xform.a * inCols + xform.c * inRows);
    *outRowsP = abs(xform.b * inCols + xform.d * inRows);
}



static void
computeXformMatrix(struct xformMatrix * const xformP, 
                   unsigned int         const sourceCols,
                   unsigned int         const sourceRows,
                   struct xformCore     const xformCore) {

    int colMax = xformCore.a * (sourceCols-1) + xformCore.c * (sourceRows-1);
    int rowMax = xformCore.b * (sourceCols-1) + xformCore.d * (sourceRows-1);

    xformP->a = xformCore.a;
    xformP->b = xformCore.b;
    xformP->c = xformCore.c;
    xformP->d = xformCore.d;
    xformP->e = colMax < 0 ? -colMax : 0;
    xformP->f = rowMax < 0 ? -rowMax : 0;
}



struct cmdlineInfo {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    const char * inputFilespec;  /* Filespec of input file */
    struct xformCore xform;
        /* Handy mathematical representation of all of transform options */
    size_t availableMemory;
    unsigned int pageSize;
    unsigned int verbose;
};



static void
interpretMemorySize(unsigned int const memsizeSpec,
                    unsigned int const memsizeOpt,
                    size_t *     const availableMemoryP) {

    size_t const sizeMax = (size_t)(-1);
    unsigned int const Meg = 1024 * 1024;

    if (memsizeSpec) {
        if (memsizeOpt > sizeMax / Meg)
            pm_error("-memsize value too large: %u MiB.  Maximum this program "
                     "can handle is %u MiB", 
                     memsizeOpt, sizeMax / Meg);
        *availableMemoryP = memsizeOpt * Meg;
    } else
        *availableMemoryP = sizeMax;
}



static void
parseCommandLine(int argc, char ** const argv,
                 struct cmdlineInfo * const cmdlineP) {
/*----------------------------------------------------------------------------
   Note that the file spec array we return is stored in the storage that
   was passed to us as the argv array.
-----------------------------------------------------------------------------*/
    optEntry * option_def;
        /* Instructions to OptParseOptions3 on how to parse our options.
         */
    optStruct3 opt;

    unsigned int option_def_index;

    unsigned int lr, tb, xy, r90, r270, r180, null;
    unsigned int memsizeSpec, pagesizeSpec, xformSpec;
    unsigned int memsizeOpt;
    const char * xformOpt;
    unsigned int xformCount;
        /* Number of transforms in the 'xformType' array */
    enum xformType xformList[10];
        /* Array of transforms to be applied, in order */

    MALLOCARRAY(option_def, 100);

    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENT3(0, "lr",        OPT_FLAG,    NULL, &lr,      0);
    OPTENT3(0, "leftright", OPT_FLAG,    NULL, &lr,      0);
    OPTENT3(0, "tb",        OPT_FLAG,    NULL, &tb,      0);
    OPTENT3(0, "topbottom", OPT_FLAG,    NULL, &tb,      0);
    OPTENT3(0, "xy",        OPT_FLAG,    NULL, &xy,      0);
    OPTENT3(0, "transpose", OPT_FLAG,    NULL, &xy,      0);
    OPTENT3(0, "r90",       OPT_FLAG,    NULL, &r90,     0);
    OPTENT3(0, "rotate90",  OPT_FLAG,    NULL, &r90,     0);
    OPTENT3(0, "ccw",       OPT_FLAG,    NULL, &r90,     0);
    OPTENT3(0, "r180",      OPT_FLAG,    NULL, &r180,    0);
    OPTENT3(0, "rotate180", OPT_FLAG,    NULL, &r180,    0);
    OPTENT3(0, "r270",      OPT_FLAG,    NULL, &r270,    0);
    OPTENT3(0, "rotate270", OPT_FLAG,    NULL, &r270,    0);
    OPTENT3(0, "cw",        OPT_FLAG,    NULL, &r270,    0);
    OPTENT3(0, "null",      OPT_FLAG,    NULL, &null,    0);
    OPTENT3(0, "verbose",   OPT_FLAG,    NULL, &cmdlineP->verbose,       0);
    OPTENT3(0, "memsize",   OPT_UINT,    &memsizeOpt, 
            &memsizeSpec,       0);
    OPTENT3(0, "pagesize",  OPT_UINT,    &cmdlineP->pageSize,
            &pagesizeSpec,      0);
    OPTENT3(0, "xform",     OPT_STRING,  &xformOpt, 
            &xformSpec, 0);

    opt.opt_table = option_def;
    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
    opt.allowNegNum = FALSE;  /* We don't parms that are negative numbers */

    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
        /* Uses and sets argc, argv, and some of *cmdlineP and others. */

    if (lr + tb + xy + r90 + r180 + r270 + null > 1)
        pm_error("You may specify only one type of flip.");
    if (lr + tb + xy + r90 + r180 + r270 + null == 1) {
        if (lr) {
            xformCount = 1;
            xformList[0] = LEFTRIGHT;
        } else if (tb) {
            xformCount = 1;
            xformList[0] = TOPBOTTOM;
        } else if (xy) {
            xformCount = 1;
            xformList[0] = TRANSPOSE;
        } else if (r90) {
            xformCount = 2;
            xformList[0] = TRANSPOSE;
            xformList[1] = TOPBOTTOM;
        } else if (r180) {
            xformCount = 2;
            xformList[0] = LEFTRIGHT;
            xformList[1] = TOPBOTTOM;
        } else if (r270) {
            xformCount = 2;
            xformList[0] = TRANSPOSE;
            xformList[1] = LEFTRIGHT;
        } else if (null) {
            xformCount = 0;
        }
    } else if (xformSpec) 
        parseXformOpt(xformOpt, &xformCount, xformList);
    else
        pm_error("You must specify an option such as -topbottom to indicate "
                 "what kind of flip you want.");

    computeXformCore(xformCount, xformList, &cmdlineP->xform);
    
    interpretMemorySize(memsizeSpec, memsizeOpt, &cmdlineP->availableMemory);

    if (!pagesizeSpec)
        cmdlineP->pageSize = 4*1024;         

    if (argc-1 == 0) 
        cmdlineP->inputFilespec = "-";
    else if (argc-1 != 1)
        pm_error("Program takes zero or one argument (filename).  You "
                 "specified %d", argc-1);
    else
        cmdlineP->inputFilespec = argv[1];
}



static void
bitOrderReverse(unsigned char * const bitrow, 
                unsigned int    const cols) {
/*----------------------------------------------------------------------------
  Reverse the bits in a packed pbm row (1 bit per pixel).  I.e. the leftmost
  bit becomes the rightmost, etc.
-----------------------------------------------------------------------------*/
    unsigned int const lastfullByteIdx = cols/8 - 1;

    if (cols == 0 || bitrow == NULL )
        pm_error("Invalid arguments passed to bitOrderReverse");

    if (cols <= 8)
        bitrow[0] = bitreverse[bitrow[0]] << (8-cols);
    else if (cols % 8 == 0) {
        unsigned int i, j;
        for (i = 0, j = lastfullByteIdx; i <= j; ++i, --j) {
            unsigned char const t = bitreverse[bitrow[j]]; 
            bitrow[j] = bitreverse[bitrow[i]];
            bitrow[i] = t;
        }
    } else {
        unsigned int const m = cols % 8; 

        unsigned int i, j;
            /* Cursors into bitrow[].  i moves from left to center;
               j moves from right to center as bits of bitrow[] are exchanged.
            */
        unsigned char th, tl;  /* 16 bit temp ( th << 8 | tl ) */
        tl = 0;
        for (i = 0, j = lastfullByteIdx+1; i <= lastfullByteIdx/2; ++i, --j) {
            th = bitreverse[bitrow[i]];
            bitrow[i] =
                bitreverse[0xff & ((bitrow[j-1] << 8 | bitrow[j]) >> (8-m))];
            bitrow[j] = 0xff & ((th << 8 | tl) >> m);
            tl = th;
        }
        if (i == j) 
            /* bitrow[] has an odd number of bytes (an even number of
               full bytes; lastfullByteIdx is odd), so we did all but
               the center byte above.  We do the center byte now.
            */
            bitrow[j] = 0xff & ((bitreverse[bitrow[i]] << 8 | tl) >> m);
    }
}



static void
transformRowByRowPbm(struct pam * const inpamP, 
                     struct pam * const outpamP,
                     bool         const reverse) {
/*----------------------------------------------------------------------------
  Transform a PBM raster either by flipping it left for right, or just
  leaving it alone, as indicated by 'reverse'.  Read the raster from
  *inpamP; write the transformed raster to *outpamP.

  Process the image one row at a time and use fast packed PBM bit
  reverse algorithm (where required).
-----------------------------------------------------------------------------*/
    unsigned char * bitrow;
    unsigned int row;

    bitrow = pbm_allocrow_packed(outpamP->width); 

    for (row = 0; row < inpamP->height; ++row) {
        pbm_readpbmrow_packed(inpamP->file,  bitrow, inpamP->width,
                              inpamP->format);

        if (reverse)
            bitOrderReverse(bitrow, inpamP->width);

        pbm_writepbmrow_packed(outpamP->file, bitrow, outpamP->width, 0);
    }
    pbm_freerow_packed(bitrow);
}



static void
transformRowByRowNonPbm(struct pam * const inpamP, 
                        struct pam * const outpamP,
                        bool         const reverse) {
/*----------------------------------------------------------------------------
  Flip a raster left for right or leave it alone.  Read the raster
  from *inpamP; write the transformed raster to *outpamP.

  Process one row at a time.

  This works on any image, but is slower and uses more memory than the
  PBM-only transformRowByRowPbm().
-----------------------------------------------------------------------------*/
    tuple * tuplerow;
    tuple * newtuplerow;
        /* This is not a full tuple row.  It is either an array of pointers
           to the tuples in 'tuplerow' (in reverse order) or just 'tuplerow'
           itself.
        */
    tuple * scratchTuplerow;
    
    unsigned int row;
    
    tuplerow = pnm_allocpamrow(inpamP);
    
    if (reverse) {
        /* Set up newtuplerow[] to point to the tuples of tuplerow[] in
           reverse order.
        */
        unsigned int col;
        
        MALLOCARRAY_NOFAIL(scratchTuplerow, inpamP->width);

        for (col = 0; col < inpamP->width; ++col) 
            scratchTuplerow[col] = tuplerow[inpamP->width - col - 1];
        newtuplerow = scratchTuplerow;
    } else {
        scratchTuplerow = NULL;
        newtuplerow = tuplerow;
    }
    for (row = 0; row < inpamP->height ; ++row) {
        pnm_readpamrow(inpamP, tuplerow);
        pnm_writepamrow(outpamP, newtuplerow);
    }
    
    if (scratchTuplerow)
        free(scratchTuplerow);
    pnm_freepamrow(tuplerow);
}



static void
transformRowsBottomTopPbm(struct pam * const inpamP,
                          struct pam * const outpamP,
                          bool         const reverse) { 
/*----------------------------------------------------------------------------
  Flip a PBM raster top for bottom; read the raster from *inpamP;
  write the flipped raster to *outpamP.

  Read complete image into memory in packed PBM format; Use fast
  packed PBM bit reverse algorithm (where required).
-----------------------------------------------------------------------------*/
    unsigned int const rows=inpamP->height;

    unsigned char ** bitrow;
    unsigned int row;
        
    bitrow = pbm_allocarray_packed(outpamP->width, outpamP->height);
        
    for (row = 0; row < rows; ++row)
        pbm_readpbmrow_packed(inpamP->file, bitrow[row], 
                              inpamP->width, inpamP->format);

    for (row = 0; row < rows; ++row) {
        if (reverse) 
            bitOrderReverse(bitrow[rows-row-1], inpamP->width);

        pbm_writepbmrow_packed(outpamP->file, bitrow[rows - row - 1],
                               outpamP->width, 0);
    }
    pbm_freearray_packed(bitrow, outpamP->height);
}



static void
transformRowsBottomTopNonPbm(struct pam * const inpamP, 
                             struct pam * const outpamP) {
/*----------------------------------------------------------------------------
  Do a simple vertical flip.  Read the raster from *inpamP; write the
  flipped raster to *outpamP.

  We do this faster than the more general subroutines because we just
  move the row pointers.

  But we must fit the entire image into memory at once.

  This works on PBM, but the PBM-specific subroutine is faster.
-----------------------------------------------------------------------------*/
    tuple ** tuplerows;
    unsigned int row;

    tuplerows = pnm_allocpamarray(outpamP);

    for (row = 0; row < inpamP->height ; ++row)
        pnm_readpamrow(inpamP, tuplerows[row]);

    for (row = 0; row < inpamP->height; ++row) {
        tuple * const tuplerow = tuplerows[inpamP->height - row - 1];

        pnm_writepamrow(outpamP, tuplerow);
    }
    
    pnm_freepamarray(tuplerows, outpamP);
}



static void __inline__
transformPoint(int                const col, 
               int                const row, 
               struct xformMatrix const xform, 
               unsigned int *     const newcolP, 
               unsigned int *     const newrowP ) {
/*----------------------------------------------------------------------------
   Compute the location in the output of a pixel that is at row 'row',
   column 'col' in the input.  Assume the output image is 'newcols' by
   'newrows' and the transformation is as described by 'xform'.

   Return the output image location of the pixel as *newcolP and *newrowP.
-----------------------------------------------------------------------------*/
    /* The transformation is:
     
                 [ a b 0 ]
       [ x y 1 ] [ c d 0 ] = [ x2 y2 1 ]
                 [ e f 1 ]

       Where (x, y) is the source pixel location and (x2, y2) is the
       target pixel location.

       Note that this is more of a logical computation than an arithmetic
       one: a, b, c, and d are -1, 0, or 1.  e is the maximum column number
       in the target image or 0; f is the maximum row number or 0.
    */
    *newcolP = xform.a * col + xform.c * row + xform.e * 1;
    *newrowP = xform.b * col + xform.d * row + xform.f * 1;

    assert(*newcolP >= 0);
    assert(*newrowP >= 0);
}



static void
writeRaster(struct pam *    const pamP,
            tuple * const * const tuples) {

    unsigned int outRow;

    for (outRow = 0; outRow < pamP->height; ++ outRow)
        pnm_writepamrow(pamP, tuples[outRow]);
}



static void
transformPbmGen(struct pam *     const inpamP,
                struct pam *     const outpamP,
                struct xformCore const xformCore) { 
/*----------------------------------------------------------------------------
   This is the same as transformGen, except that it uses less 
   memory, since the PBM buffer format uses one bit per pixel instead
   of twelve bytes + pointer space

   This can do any PBM transformation, but is slower and uses more
   memory than the more restricted transformRowByRowPbm() and
   transformRowsBottomTopPbm().
-----------------------------------------------------------------------------*/
    bit * bitrow;
    bit ** newbits;
    struct xformMatrix xform;
    unsigned int row;
            
    computeXformMatrix(&xform, inpamP->width, inpamP->height, xformCore);
    
    bitrow = pbm_allocrow(inpamP->width);
    newbits = pbm_allocarray(pbm_packed_bytes(outpamP->width), 
                             outpamP->height);
            
    /* Initialize entire array to zeroes.  One bits will be or'ed in later */
    for (row = 0; row < outpamP->height; ++row) {
        unsigned int col;
        for (col = 0; col < pbm_packed_bytes(outpamP->width); ++col) 
             newbits[row][col] = 0; 
    }
    
    for (row = 0; row < inpamP->height; ++row) {
        unsigned int col;
        pbm_readpbmrow(inpamP->file, bitrow, inpamP->width, inpamP->format);
        for (col = 0; col < inpamP->width; ++col) {
            unsigned int newcol, newrow;
            transformPoint(col, row, xform, &newcol, &newrow);
            newbits[newrow][newcol/8] |= bitrow[col] << (7 - newcol % 8);
                /* Use of "|=" patterned after pbm_readpbmrow_packed. */
         }
    }
    
    for (row = 0; row < outpamP->height; ++row)
        pbm_writepbmrow_packed(outpamP->file, newbits[row], outpamP->width, 0);
    
    pbm_freearray(newbits, outpamP->height);
    pbm_freerow(bitrow);
}



static void
transformNonPbmWhole(struct pam *     const inpamP,
                     struct pam *     const outpamP,
                     struct xformCore const xformCore,
                     bool             const verbose) {
/*----------------------------------------------------------------------------
  Do the transform using "pam" library functions, as opposed to "pbm"
  ones.

  Read the raster from *inpamP; write the transformed raster to *outpamP.

  Assume input file is positioned to the raster (just after the
  header).

  This can do any transformation, but is slower and uses more memory
  than some of the alternatives which are usable only for certain
  cases.  But we do require a certain amount of virtual and real memory;
  transformNonPbmChunk() is even more general in that it doesn't.
-----------------------------------------------------------------------------*/
    tuple * tuplerow;
    tuple ** newtuples;
    struct xformMatrix xform;
    unsigned int row;

    computeXformMatrix(&xform, inpamP->width, inpamP->height, xformCore);
    
    tuplerow = pnm_allocpamrow(inpamP);
    newtuples = pnm_allocpamarray(outpamP);
    
    for (row = 0; row < inpamP->height; ++row) {
        unsigned int col;
        pnm_readpamrow(inpamP, tuplerow);
        
        for (col = 0; col < inpamP->width; ++col) {
            unsigned int newcol, newrow;

            transformPoint(col, row, xform, &newcol, &newrow);

            assert(newcol < outpamP->width);
            assert(newrow < outpamP->height);

            pnm_assigntuple(inpamP, newtuples[newrow][newcol],
                            tuplerow[col]);
        }
    }
    
    writeRaster(outpamP, newtuples);
    
    pnm_freepamarray(newtuples, outpamP);
    pnm_freepamrow(tuplerow);
}



typedef struct {
/*----------------------------------------------------------------------------
   A description of the quilt of cells that make up the output image.
-----------------------------------------------------------------------------*/
    unsigned int ranks, files;
        /* Dimensions of the output image in cells */
    struct pam ** pam;
        /* pam[y][x] is the pam structure for the cell at rank y, file x
           in the output.
        */

    /* Each of the cells corresponds to a temporary file; the 'file'
       member of its pam structure identifies it.  But it is not a normal
       Netpbm file; it contains only the raster portion.  The program
       writes the raster to the file, starting at offset zero, then rewinds
       and reads it out later.  The header is unnecessary because the pam
       structure is still available at readback time.
    */
} outputMap;



static void
initOutCell(struct pam *     const outCellPamP,
            unsigned int     const inCellWidth,
            unsigned int     const inCellHeight,
            struct pam *     const inpamP,
            struct xformCore const xformCore) {

    unsigned int outCellFiles, outCellRanks;

    *outCellPamP = *inpamP;  /* initial value */

    outCellPamP->len  = PAM_STRUCT_SIZE(tuple_type);

    outCellPamP->file = pm_tmpfile();

    xformDimensions(xformCore, inCellWidth, inCellHeight,
                    &outCellFiles, &outCellRanks);

    outCellPamP->width = outCellFiles;
    outCellPamP->height = outCellRanks;
}



static outputMap *
createOutputMap(struct pam *       const inpamP,
                unsigned int       const maxRows,
                struct xformMatrix const cellXform,
                struct xformCore   const xformCore) {

    unsigned int const inCellFiles = 1;
    unsigned int const inCellRanks = (inpamP->height + maxRows - 1) / maxRows;

    outputMap * mapP;
    unsigned int outCellRank;
    unsigned int inCellRank;

    MALLOCVAR_NOFAIL(mapP);

    xformDimensions(xformCore, inCellFiles, inCellRanks,
                    &mapP->files, &mapP->ranks);

    MALLOCARRAY(mapP->pam, mapP->ranks);
    if (mapP->pam == NULL)
        pm_error("Could not allocate a cell array for %u ranks of cells.",
                 mapP->ranks);

    for (outCellRank = 0; outCellRank < mapP->ranks; ++outCellRank) {

        MALLOCARRAY(mapP->pam[outCellRank], mapP->files);

        if (mapP->pam[outCellRank] == NULL)
            pm_error("Failed to allocate rank %u of the cell array, "
                     "%u cells wide", outCellRank, mapP->files);
    }

    for (inCellRank = 0; inCellRank < inCellRanks; ++inCellRank) {
        unsigned int const inCellFile = 0;
        unsigned int const inCellStartRow = inCellRank * maxRows;
        unsigned int const inCellRows =
            MIN(inpamP->height - inCellStartRow, maxRows);

        unsigned int outCellFile, outCellRank;
        transformPoint(inCellFile, inCellRank, cellXform,
                       &outCellFile, &outCellRank);
    
        initOutCell(&mapP->pam[outCellRank][outCellFile],
                    inpamP->width, inCellRows,
                    inpamP, xformCore);
    }
    return mapP;
}
                


static void
destroyOutputMap(outputMap * const mapP) {

    unsigned int outCellRank;

    for (outCellRank = 0; outCellRank < mapP->ranks; ++outCellRank)
        free(mapP->pam[outCellRank]);

    free(mapP->pam);

    free(mapP);
}



static void
rewindCellFiles(outputMap * const outputMapP) {

    unsigned int outCellRank;

    for (outCellRank = 0; outCellRank < outputMapP->ranks; ++outCellRank) {
        unsigned int outCellFile;
        for (outCellFile = 0; outCellFile < outputMapP->files; ++outCellFile)
            pm_seek(outputMapP->pam[outCellRank][outCellFile].file, 0);
    }
}



static void
closeCellFiles(outputMap * const outputMapP) {

    unsigned int outCellRank;

    for (outCellRank = 0; outCellRank < outputMapP->ranks; ++outCellRank) {
        unsigned int outCellFile;
        for (outCellFile = 0; outCellFile < outputMapP->files; ++outCellFile)
            pm_close(outputMapP->pam[outCellRank][outCellFile].file);
    }
}



static void
transformCell(struct pam *     const inpamP,
              struct pam *     const outpamP,
              struct xformCore const xformCore) {

    struct xformMatrix xform;
    tuple * inTupleRow;
    tuple ** outTuples;
    unsigned int inRow;

    computeXformMatrix(&xform, inpamP->width, inpamP->height, xformCore);

    inTupleRow = pnm_allocpamrow(inpamP);

    outTuples = pnm_allocpamarray(outpamP);

    for (inRow = 0; inRow < inpamP->height; ++inRow) {
        unsigned int inCol;

        pnm_readpamrow(inpamP, inTupleRow);
        
        for (inCol = 0; inCol < inpamP->width; ++inCol) {
            unsigned int outCol, outRow;

            transformPoint(inCol, inRow, xform, &outCol, &outRow);

            assert(outCol < outpamP->width);
            assert(outRow < outpamP->height);

            pnm_assigntuple(inpamP,
                            outTuples[outRow][outCol], inTupleRow[inCol]);
        }
    }
    pnm_freepamrow(inTupleRow);

    writeRaster(outpamP, outTuples);

    pnm_freepamarray(outTuples, outpamP);
}



static void
stitchCellsToOutput(outputMap *  const outputMapP,
                    struct pam * const outpamP) {

    unsigned int outRank;
    tuple * tupleRow;

    tupleRow = pnm_allocpamrow(outpamP);

    for (outRank = 0; outRank < outputMapP->ranks; ++outRank) {
        unsigned int const cellRows = outputMapP->pam[outRank][0].height;
            /* Number of rows in every cell in this rank */

        unsigned int cellRow;

        for (cellRow = 0; cellRow < cellRows; ++cellRow) {
            unsigned int outFile;
            unsigned int outCol;

            outCol = 0;

            for (outFile = 0; outFile < outputMapP->files; ++outFile) {
                struct pam * const outCellPamP = 
                    &outputMapP->pam[outRank][outFile];

                assert(outCellPamP->height == cellRows);

                assert(outCol < outpamP->width);
                pnm_readpamrow(outCellPamP, &tupleRow[outCol]);

                outCol += outCellPamP->width;
            }

            assert(outCol = outpamP->width);

            pnm_writepamrow(outpamP, tupleRow);
        }
    }
    pnm_freepamrow(tupleRow);
}



static void
transformNonPbmChunk(struct pam *     const inpamP,
                     struct pam *     const outpamP,
                     struct xformCore const xformCore,
                     unsigned int     const maxRows,
                     bool             const verbose) {
/*----------------------------------------------------------------------------
  Same as transformNonPbmChunk(), except we read 'maxRows' rows of the
  input into memory at a time, storing intermediate results in temporary
  files, to limit our use of virtual and real memory.

  Assume input file is positioned to the raster (just after the
  header).

  We call the strip of 'maxRows' rows that we read a source cell.  We
  transform that cell according to 'xformCore' to create to create a
  target cell.  We store all the target cells in temporary files.
  We consider the target cells to be arranged in a column matrix the
  same as the source cells within the source image; we transform that
  matrix according to 'xformCore'.  The resulting cell matrix is the
  target image.
-----------------------------------------------------------------------------*/
    unsigned int const inCellFiles = 1;
    unsigned int const inCellRanks = (inpamP->height + maxRows - 1) / maxRows;

    struct xformMatrix cellXform;
    unsigned int inCellRank;
    outputMap * outputMapP;

    if (verbose)
        pm_message("Transforming in %u chunks, using temp files", inCellRanks);

    computeXformMatrix(&cellXform, inCellFiles, inCellRanks, xformCore);

    outputMapP = createOutputMap(inpamP, maxRows, cellXform, xformCore);

    for (inCellRank = 0; inCellRank < inCellRanks; ++inCellRank) {
        unsigned int const inCellFile = 0;
        unsigned int const inCellStartRow = inCellRank * maxRows;
        unsigned int const inCellRows =
            MIN(inpamP->height - inCellStartRow, maxRows);

        struct pam inCellPam;
        struct pam * outCellPamP;
        unsigned int outCellFile, outCellRank;

        transformPoint(inCellFile, inCellRank, cellXform,
                       &outCellFile, &outCellRank);
    
        outCellPamP = &outputMapP->pam[outCellRank][outCellFile];

        /* Input cell is just the next 'inCellRows' rows of input image */
        inCellPam = *inpamP;
        inCellPam.height = inCellRows;

        transformCell(&inCellPam, outCellPamP, xformCore);
    }    

    rewindCellFiles(outputMapP);

    stitchCellsToOutput(outputMapP, outpamP);

    closeCellFiles(outputMapP);

    destroyOutputMap(outputMapP);
}



static unsigned int
maxRowsThatFit(struct pam * const pamP,
               size_t       const availableMemory) {

    unsigned long const otherNeeds = 100*1024;
        /* A wild guess at how much memory (from the same pool as the
           input rows) is needed for things other than the input rows
        */
    unsigned long const availForRows =
        availableMemory > otherNeeds ? availableMemory - otherNeeds : 0;
    unsigned int const bytesPerTuple =
        pamP->depth * sizeof(sample) + sizeof(tuple *);
    unsigned int const bytesPerRow =
        pamP->width * bytesPerTuple + sizeof(tuple **);

    unsigned long const maxRows = availForRows / bytesPerRow;

    if (maxRows < 1)
        pm_error("You haven't allowed enough memory to fit even one row "
                 "of the source image in memory.  The minimum chunk size "
                 "is one row; we need at least %lu bytes.",
                 otherNeeds + bytesPerRow);

    return (unsigned int)MIN(maxRows, UINT_MAX);
}



static void
transformPbm(struct pam *     const inpamP,
             struct pam *     const outpamP,
             struct xformCore const xform,
             bool             const verbose) {

    if (xform.b == 0 && xform.c == 0) {
        /* Rows of input map to rows of target; no column-for-row */
        if (xform.d == 1)
            /* Row N of the output is based only on Row N of the
               input, so we can transform row by row and avoid
               in-memory buffering altogether.
            */
            transformRowByRowPbm(inpamP, outpamP, xform.a == -1);
        else
            /* Row N of the output is based only on Row ~N of the
               input.  We need all the rows in memory, but have to pass
               through them only twice, so there is no page thrashing concern.
            */
            transformRowsBottomTopPbm(inpamP, outpamP, xform.a == -1);
    } else
        /* This is a column-for-row type of transformation, which requires
           complex traversal of an in-memory image.
        */
        transformPbmGen(inpamP, outpamP, xform);
}



static void
transformNonPbm(struct pam *     const inpamP,
                struct pam *     const outpamP,
                struct xformCore const xform,
                unsigned int     const availableMemory,
                bool             const verbose) {

    if (xform.b == 0 && xform.c == 0 && xform.d == 1) {
        /* Row N of the output is based only on Row N of the
           input, so we can transform row by row and avoid
           in-memory buffering altogether.
        */
        if (verbose)
            pm_message("Transforming row by row");
        transformRowByRowNonPbm(inpamP, outpamP, xform.a == -1);
    } else {
        unsigned int const maxRows = maxRowsThatFit(inpamP, availableMemory);
        if (maxRows >= inpamP->height) {
            /* We can fit the whole image in memory at once and avoid
               temporary files.
            */
            if (xform.b == 0 && xform.c == 0 && xform.d == -1 &&
                xform.a == 1) {
                /* This is just a vertical flip;  We can move whole rows
                   instead of individual pixels and save time.
                */
                if (verbose)
                    pm_message("Transforming whole rows, all in memory");

                transformRowsBottomTopNonPbm(inpamP, outpamP);
            } else {
                if (verbose)
                    pm_message("Transforming whole image at once, "
                               "pixel by pixel");
                transformNonPbmWhole(inpamP, outpamP, xform, verbose);
            }
        } else
            /* No optimizations possible */
            transformNonPbmChunk(inpamP, outpamP, xform, maxRows, verbose);
    }
}



int
main(int argc, char * argv[]) {
    struct cmdlineInfo cmdline;
    struct pam inpam;
    struct pam outpam;
    unsigned int cols, rows;
    FILE * ifP;

    pnm_init(&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    if (cmdline.availableMemory < UINT_MAX)
        ifP = pm_openr_seekable(cmdline.inputFilespec);
    else
        ifP = pm_openr(cmdline.inputFilespec);
    
    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));

    outpam = inpam;  /* initial value */

    outpam.file = stdout;
    xformDimensions(cmdline.xform, inpam.width, inpam.height, &cols, &rows);
    outpam.width = cols; outpam.height = rows;

    pnm_writepaminit(&outpam);

    switch (PNM_FORMAT_TYPE(inpam.format)) {
    case PBM_TYPE:
        transformPbm(&inpam, &outpam, cmdline.xform, cmdline.verbose);
        break;
    default:
        transformNonPbm(&inpam, &outpam, cmdline.xform,
                        cmdline.availableMemory, cmdline.verbose);
    }
    pm_close(inpam.file);
    pm_close(outpam.file);
    
    return 0;
}
