/* pbmtomrf - convert pbm to mrf
 * public domain by RJM
 *
 * Adapted to Netpbm by Bryan Henderson 2003.08.09.  Bryan got his copy from
 * ftp://ibiblio.org/pub/linux/apps/convert, dated 1998.03.03.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>

#include "pm_c_util.h"
#include "pbm.h"



typedef struct bitOut {
    int bitbox;
    int bitsleft;
    FILE * fileP;
} bitOut;



static void
bit_init(struct bitOut * const bitOutP,
         FILE *          const ofP) {

    bitOutP->bitbox = 0;
    bitOutP->bitsleft = 8;
    bitOutP->fileP = ofP;
}



static void
bit_output(struct bitOut * const bitOutP,
           int             const bit) {

    --bitOutP->bitsleft;
    bitOutP->bitbox |= (bit << bitOutP->bitsleft);
    if (bitOutP->bitsleft == 0) {
        fputc(bitOutP->bitbox, bitOutP->fileP);
        bitOutP->bitbox = 0;
        bitOutP->bitsleft = 8;
    }
}



static void
bit_flush(struct bitOut * const bitOutP) {
    /* there are never 0 bits left outside of bit_output, but
     * if 8 bits are left here there's nothing to flush, so
     * only do it if bitsleft!=8.
     */
    if (bitOutP->bitsleft != 8) {
        bitOutP->bitsleft = 1;
        bit_output(bitOutP, 0);    /* yes, really. This will always work. */
    }
}



static void
determineBlackWhiteOrMix(const unsigned char * const image,
                         unsigned int          const ulCol,
                         unsigned int          const ulRow,
                         unsigned int          const imageWidth,
                         unsigned int          const size,
                         bool *                const oneColorP,
                         int *                 const colorP) {
/*----------------------------------------------------------------------------
   Determine whether a square within 'image' is all white, all black,
   or a mix.
-----------------------------------------------------------------------------*/
    unsigned int rowOfSquare;
    unsigned int t;

    for (rowOfSquare = 0, t = 0; rowOfSquare < size; ++rowOfSquare) {
        unsigned int colOfSquare;
        for (colOfSquare = 0; colOfSquare < size; ++colOfSquare) {
            unsigned int rowOfImage = ulRow + rowOfSquare;
            unsigned int colOfImage = ulCol + colOfSquare;

            t += image[rowOfImage * imageWidth + colOfImage];
        }
    }
    /* if the total's 0, it's black. if it's size*size, it's white. */
    if (t == 0) {
        *oneColorP = TRUE;
        *colorP = 0;
    } else if (t == SQR(size)) {
        *oneColorP = TRUE;
        *colorP = 1;
    } else
        *oneColorP = FALSE;
}



static void
doSquare(bitOut *              const bitOutP,
         const unsigned char * const image,
         unsigned int          const ulCol,
         unsigned int          const ulRow,
         unsigned int          const imageWidth,
         unsigned int          const size) {
/*----------------------------------------------------------------------------
   Do a square of side 'size', whose upper left corner is at (ulCol, ulRow).
   This is a square within 'image', which is a concatenation of rows
   'imageWidth' pixels wide, one byte per pixel.

   Write the pixel values out to the bit stream *bitOutP, in MRF format.
-----------------------------------------------------------------------------*/
    if (size == 1) {
        /* The fact that it is all one color is implied because the square is
           just one pixel; no bit goes in MRF output to state that.
        */
        bit_output(bitOutP, image[ulRow * imageWidth + ulCol] ? 1 : 0);
    } else {
        bool oneColor;
        int color;

        determineBlackWhiteOrMix(image, ulCol, ulRow, imageWidth, size,
                                 &oneColor, &color);

        if (oneColor) {
            bit_output(bitOutP, 1);  /* all same color */
            bit_output(bitOutP, color);
        } else {
            /* Square is not all the same color, so recurse.  Do each
               of the four quadrants of this square individually.
            */
            unsigned int const quadSize = size/2;

            bit_output(bitOutP, 0);    /* not all same color */

            doSquare(bitOutP, image, ulCol,            ulRow,
                     imageWidth, quadSize);
            doSquare(bitOutP, image, ulCol + quadSize, ulRow,
                     imageWidth, quadSize);
            doSquare(bitOutP, image, ulCol,            ulRow + quadSize,
                     imageWidth, quadSize);
            doSquare(bitOutP, image, ulCol + quadSize, ulRow + quadSize,
                     imageWidth, quadSize);
        }
    }
}



static void
fiddleRightEdge(unsigned char * const image,
                unsigned int    const w,
                unsigned int    const h,
                unsigned int    const pw,
                bool *          const flippedP) {

    unsigned int row;
    unsigned int t;

    for (row = t = 0; row < h; ++row)
        t += image[row*pw + w - 1];

    if (t*2 > h) {
        unsigned int row;

        *flippedP = TRUE;
        for (row = 0; row < h; ++row) {
            unsigned int col;
            for (col = w; col < pw; ++col)
                image[row*pw + col] = 1;
        }
    } else
        *flippedP = FALSE;
}



static void
fiddleBottomEdge(unsigned char * const image,
                 unsigned int    const w,
                 unsigned int    const h,
                 unsigned int    const pw,
                 unsigned int    const ph,
                 bool *          const flippedP) {

    unsigned int col;
    unsigned int t;

    for (col = t = 0; col < w; ++col)
        t += image[(h-1)*pw + col];

    if (t*2 > w) {
        unsigned int row;
        *flippedP = TRUE;
        for (row = h; row < ph; ++row) {
            unsigned int col;
            for (col = 0; col < w; ++col)
                image[row*pw + col] = 1;
        }
    } else
        *flippedP = FALSE;
}



static void
fiddleBottomRightCorner(unsigned char * const image,
                        unsigned int    const w,
                        unsigned int    const h,
                        unsigned int    const pw,
                        unsigned int    const ph) {
    unsigned int row;

    for (row = h; row < ph; ++row) {
        unsigned int col;

        for (col = w; col < pw; ++col)
                    image[row*pw + col] = 1;
    }
}



static void
fiddleEdges(unsigned char * const image,
            int             const cols,
            int             const rows) {
/* the aim of this routine is play around with the edges which
 * are compressed into the mrf but thrown away when it's decompressed,
 * such that we get the best compression possible.
 * If you don't see why this is a good idea, consider the simple case
 * of a 1x1 white pixel. Placed on a black 64x64 this takes several bytes
 * to compress. On a white 64x64, it takes two bits.
 * (Clearly most cases will be more complicated, but you should get the
 * basic idea from that.)
 */

    /* there are many possible approaches to this problem, and this one's
         * certainly not the best, but at least it's quick and easy, and it's
         * better than nothing. :-)
         *
         * So, all we do is flip the runoff area of an edge to white
         * if more than half of the pixels on that edge are
         * white. Then for the bottom-right runoff square (if there is
         * one), we flip it if we flipped both edges.
         */

    /* w64 is units-of-64-bits width, h64 same for height */
    unsigned int const w64 = (cols + 63) / 64;
    unsigned int const h64 = (rows + 63) / 64;

    int const pw=w64*64;
    int const ph=h64*64;

    bool flippedRight, flippedBottom;

    if (cols % 64 != 0)
        fiddleRightEdge(image, cols, rows, pw, &flippedRight);
    else
        flippedRight = FALSE;

    if (rows % 64 != 0)
        fiddleBottomEdge(image, cols, rows, pw, ph, &flippedBottom);
    else
        flippedBottom = FALSE;

    if (flippedRight && flippedBottom)
        fiddleBottomRightCorner(image, cols, rows, pw, ph);
}



static void
readPbmImage(FILE *           const ifP,
             unsigned char ** const imageP,
             int *            const colsP,
             int *            const rowsP) {


    /* w64 is units-of-64-bits width, h64 same for height */
    unsigned int w64, h64;

    unsigned char * image;
    int cols, rows, format;
    unsigned int row;
    bit * bitrow;

    pbm_readpbminit(ifP, &cols, &rows, &format);

    w64 = (cols + 63) / 64;
    h64 = (rows + 63) / 64;

    if (UINT_MAX/w64/64/h64/64 == 0)
        pm_error("Ridiculously large, unprocessable image: %u cols x %u rows",
                 cols, rows);

    image = calloc(w64*h64*64*64, 1);
    if (image == NULL)
        pm_error("Unable to get memory for raster");

    /* get bytemap image rounded up into mod 64x64 squares */

    bitrow = pbm_allocrow(cols);

    for (row = 0; row < rows; ++row) {
        unsigned int col;

        pbm_readpbmrow(ifP, bitrow, cols, format);

        for (col =0; col < cols; ++col)
            image[row*(w64*64) + col] = (bitrow[col] == PBM_WHITE ? 1 : 0);
    }
    pbm_freerow(bitrow);
    *imageP = image;
    *colsP = cols;
    *rowsP = rows;
}



static void
outputMrf(FILE *          const ofP,
          unsigned char * const image,
          unsigned int    const cols,
          unsigned int    const rows) {

    unsigned int const w64 = (cols + 63) / 64;
    unsigned int const h64 = (rows + 63) / 64;

    bitOut bitOut;

    unsigned int row;

    fprintf(ofP, "MRF1");
    fprintf(ofP, "%c%c%c%c", cols >> 24, cols >> 16, cols >> 8, cols >> 0);
    fprintf(ofP, "%c%c%c%c", rows >> 24, rows >> 16, rows >> 8, rows >> 0);
    fputc(0, ofP);   /* option byte, unused for now */

    /* now recursively check squares. */

    bit_init(&bitOut, ofP);

    for (row = 0; row < h64; ++row) {
        unsigned int col;
        for (col = 0; col < w64; ++col)
            doSquare(&bitOut, image, col*64, row*64, w64*64, 64);
    }
    bit_flush(&bitOut);
}



int
main(int argc,char *argv[]) {

    FILE * ifP;
    FILE * ofP;
    unsigned char *image;
    int rows, cols;

    pbm_init(&argc, argv);

    if (argc-1 > 1)
        pm_error("Too many arguments: %d.  Only argument is input file",
                 argc-1);

    if (argc-1 == 1)
        ifP = pm_openr(argv[1]);
    else
        ifP = stdin;

    ofP = stdout;

    readPbmImage(ifP, &image, &cols, &rows);

    pm_close(ifP);

    /* if necessary, alter the unused outside area to aid compression of
     * edges of image.
     */

    fiddleEdges(image, cols, rows);

    outputMrf(ofP, image, cols, rows);

    free(image);

    return 0;
}



