/******************************************************************************
                             palmtopnm
*******************************************************************************
  By Bryan Henderson, San Jose, California, June 2004.

  Inspired by and using methods from Tbmptopnm by Ian Goldberg
  <iang@cs.berkeley.edu>, and Bill Janssen <bill@janssen.org>.

  Bryan's work is contributed to the public domain by its author.
******************************************************************************/

#include <assert.h>

#include "pnm.h"
#include "shhopt.h"
#include "mallocvar.h"

#include "palm.h"



enum palmCompressionType {
    COMPRESSION_NONE, 
    COMPRESSION_RLE, 
    COMPRESSION_SCANLINE,
    COMPRESSION_PACKBITS
};

struct palmHeader {
    unsigned short cols;
    unsigned short rows;
    unsigned short bytesPerRow;
    unsigned short flags;
    unsigned char  pixelSize;
    unsigned char  version;
    unsigned char  transparentIndex;
    enum palmCompressionType compressionType;
};

struct directColorInfo {
    unsigned int redbits;
    unsigned int greenbits;
    unsigned int bluebits;
    Color_s      transparentColor;
};




struct cmdlineInfo {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    const char * inputFilespec;
    unsigned int verbose;
    unsigned int rendition;
    unsigned int showhist;
    unsigned int transparent;
};


static void
parseCommandLine(int argc, char ** argv,
                 struct cmdlineInfo *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 = malloc( 100*sizeof( optEntry ) );
        /* Instructions to optParseOptions3 on how to parse our options.
         */
    optStruct3 opt;

    unsigned int renditionSpec;

    unsigned int option_def_index;

    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENT3(0, "verbose",     OPT_FLAG, NULL,
            &cmdlineP->verbose,  0);
    OPTENT3(0, "showhist",    OPT_FLAG, NULL,
            &cmdlineP->showhist, 0);
    OPTENT3(0, "transparent",    OPT_FLAG, NULL,
            &cmdlineP->transparent, 0);
    OPTENT3(0, "rendition",  OPT_UINT, &cmdlineP->rendition, 
            &renditionSpec, 0);

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

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


    if (renditionSpec) {
        if (cmdlineP->rendition < 1)
            pm_error("The -rendition value must be at least 1");
    } else 
        cmdlineP->rendition = 1;
    
    if (cmdlineP->transparent) {
        if (renditionSpec)
            pm_error("You can't specify -rendition with -transparent");
        if (cmdlineP->showhist)
            pm_error("You can't specify -showhist with -transparent");
    }

    if (argc-1 < 1)
        cmdlineP->inputFilespec = "-";
    else {
        cmdlineP->inputFilespec = argv[1];
        if (argc-1 > 1)
            pm_error("Too many arguments (%d).  The only non-option "
                     "argument is the file name", argc-1);
    }
}



static xelval *
createGraymap(unsigned int const ncolors, 
              unsigned int const minval, 
              xelval       const maxval) {
    int i;
    xelval *map;

    MALLOCARRAY_NOFAIL(map, ncolors);
    for (i = 0; i < ncolors; ++i) {
        map[i] = maxval - (i * (maxval - minval)) / (ncolors - 1);
    }
    return map;
}



static void 
skipbytes(FILE *       const ifP, 
          unsigned int const nbytes) {

    unsigned char buf[256];
    unsigned int n;

    n = nbytes;  /* initial value */

    while (n > 0) {
        if (n > sizeof(buf)) {
            fread(buf, sizeof(buf), 1, ifP);
            n -= sizeof(buf);
        } else {
            fread(buf, n, 1, ifP);
            n = 0;
        }
    }    
}



static void
interpretCompression(unsigned short             const flags,
                     unsigned char              const compressionValue,
                     enum palmCompressionType * const compressionTypeP) {
    
    switch (compressionValue) {
    case PALM_COMPRESSION_RLE:
        *compressionTypeP = COMPRESSION_RLE;
        break;
    case PALM_COMPRESSION_SCANLINE:
        *compressionTypeP = COMPRESSION_SCANLINE;
        break;
    case PALM_COMPRESSION_PACKBITS:
        *compressionTypeP = COMPRESSION_PACKBITS;
        break;
    case PALM_COMPRESSION_NONE:
        *compressionTypeP = COMPRESSION_NONE;
        break;
    default:
        pm_error("The Palm image header has an unrecognized value for "
                 "compression type: 0x%02x", (unsigned)compressionValue);
    }
}



static void
readHeader(FILE *              const ifP,
           unsigned int        const requestedRendition,
           struct palmHeader * const palmHeaderP) {
    
    bool gotHeader;
    unsigned int currentRendition;

    gotHeader = FALSE;
    currentRendition = 1;
    while (!gotHeader) {
        short cols, rows, bytesPerRow, flags, nextDepthOffset;
        unsigned char pixelSize, version;

        pm_readbigshort(ifP, &cols);
        pm_readbigshort(ifP, &rows);
        pm_readbigshort(ifP, &bytesPerRow);
        pm_readbigshort(ifP, &flags);
        pixelSize = fgetc(ifP);
        version = fgetc(ifP);
        pm_readbigshort(ifP, &nextDepthOffset);
        if (currentRendition < requestedRendition) {
            if (nextDepthOffset == 0)
                pm_error("Not enough renditions in the input pixmap "
                         "to extract the %dth", requestedRendition);
            /* nextDepthOffset is calculated in 4 byte words
             * from the beginning of this bitmap (so it equals it's size) */
            skipbytes(ifP, (nextDepthOffset*4)-12);
            ++currentRendition;
        } else {
            short pad;
            unsigned char compressionType;

            palmHeaderP->transparentIndex = fgetc(ifP);
            compressionType = fgetc(ifP);
            
            pm_readbigshort(ifP, &pad);	/* reserved by Palm as of 8/9/00 */

            gotHeader = TRUE;
            palmHeaderP->cols = cols;
            palmHeaderP->rows = rows;
            palmHeaderP->bytesPerRow = bytesPerRow;
            palmHeaderP->flags = flags;
            palmHeaderP->pixelSize = pixelSize;
            palmHeaderP->version = version;

            interpretCompression(palmHeaderP->flags, compressionType,
                                 &palmHeaderP->compressionType);
        }
    }
}



static void
reportPalmHeader(struct palmHeader const palmHeader) {

    const char *ctype;

    switch (palmHeader.compressionType) {
    case COMPRESSION_RLE:
        ctype = "rle (Palm OS 3.5)";
        break;
    case COMPRESSION_SCANLINE:
        ctype = "scanline (Palm OS 2.0)";
        break;
    case COMPRESSION_PACKBITS:
        ctype = "packbits (Palm OS 4.0)";
        break;
    case COMPRESSION_NONE:
        ctype = "none";
        break;
    }
    pm_message("Dimensions: %hu columns x %hu rows",
               palmHeader.cols, palmHeader.rows);
    pm_message("Row layout: %hu bytes per row, %hu bits per pixel",
               palmHeader.bytesPerRow, palmHeader.pixelSize);
    pm_message("Flags: 0x%04hx", palmHeader.flags);
    pm_message("Version %d", palmHeader.version);
    pm_message("Transparent index: %d", palmHeader.transparentIndex);
    pm_message("Compression type: %s", ctype);
}




static void
determineOutputFormat(struct palmHeader const palmHeader,
                      int *             const formatP,
                      xelval *          const maxvalP) {

    if (palmHeader.flags & PALM_DIRECT_COLOR) {
        *formatP = PPM_TYPE;
        *maxvalP = 255;
    } else if (palmHeader.flags & PALM_HAS_COLORMAP_FLAG) {
        *formatP = PPM_TYPE;
        *maxvalP = 255;
    } else if (palmHeader.pixelSize == 1) {
        *formatP = PBM_TYPE;
        *maxvalP = 1;
    } else if (palmHeader.pixelSize >= 8) {
        *formatP = PPM_TYPE;
        *maxvalP = pm_bitstomaxval(palmHeader.pixelSize);
    } else {
        *formatP = PGM_TYPE;
        *maxvalP = pm_bitstomaxval(palmHeader.pixelSize);
    }
}



static void
getColorInfo(struct palmHeader        const palmHeader,
             FILE *                   const ifP,
             Colormap *               const colormapP,
             unsigned int *           const ncolorsP,
             struct directColorInfo * const directColorInfoP) {
             
    if (palmHeader.flags & PALM_DIRECT_COLOR) {
        *colormapP = NULL;

        directColorInfoP->redbits   = fgetc(ifP);
        directColorInfoP->greenbits = fgetc(ifP);
        directColorInfoP->bluebits  = fgetc(ifP);
        fgetc(ifP);  /* padding */
        fgetc(ifP);  /* padding */
        directColorInfoP->transparentColor = 
            (fgetc(ifP) << 16) | (fgetc(ifP) << 8) | fgetc(ifP);
    } else if (palmHeader.flags & PALM_HAS_COLORMAP_FLAG)
        *colormapP = palmcolor_read_colormap (ifP);
    else if (palmHeader.pixelSize >= 8) {
        Colormap colormap;
        colormap = palmcolor_build_default_8bit_colormap();
        qsort(colormap->color_entries, colormap->ncolors, 
              sizeof(Color_s), palmcolor_compare_indices);
        *colormapP = colormap;
    } else
        *colormapP = NULL;

    *ncolorsP = 1 << palmHeader.pixelSize;
}



static void
doTransparent(unsigned short         const flags,
              unsigned char          const transparentIndex,
              unsigned char          const pixelSize,
              Colormap               const colormap,
              struct directColorInfo const directColorInfo) {

    if (flags & PALM_HAS_TRANSPARENCY_FLAG) {
        if (colormap) {
            Color_s const color = transparentIndex << 24;
            Color const actualColor = (bsearch(&color,
                                               colormap->color_entries, 
                                               colormap->ncolors,
                                               sizeof(color), 
                                               palmcolor_compare_indices));
            printf("#%02x%02x%02x\n", 
                   (unsigned int) ((*actualColor >> 16) & 0xFF),
                   (unsigned int) ((*actualColor >>  8) & 0xFF), 
                   (unsigned int) ((*actualColor >>  0 )& 0xFF));
        } else if (flags & PALM_DIRECT_COLOR) {
            Color_s const color = directColorInfo.transparentColor;
            printf("#%02x%02x%02x\n", 
                   (unsigned int)((color >> 16) & 0xFF), 
                   (unsigned int)((color >>  8) & 0xFF), 
                   (unsigned int)((color >>  0 )& 0xFF));
        } else {
            unsigned int const maxval = pm_bitstomaxval(pixelSize);
            unsigned int const grayval = 
                ((maxval - transparentIndex) * 256) / maxval;
            printf("#%02x%02x%02x\n", grayval, grayval, grayval);
        }
    }
}




static void
createHistogram(unsigned int    const ncolors,
                unsigned int ** const seenP) {

    unsigned int * seen;

    MALLOCARRAY(seen, ncolors);
    if (!seen)
        pm_error("Can't allocate array for keeping track of "
                 "how many pixels of each of %u colors are in the image.", 
                 ncolors);

    {    
        /* Initialize the counter for each color to zero */
        unsigned int i;
        for (i = 0; i < ncolors; ++i)
            seen[i] = 0;
    }
    *seenP = seen;
}



static void
readRleRow(FILE *          const ifP,
           unsigned char * const palmrow,
           unsigned int    const bytesPerRow) {

    unsigned int j;
    for (j = 0;  j < bytesPerRow; ) {
        unsigned int const incount = fgetc(ifP);
        unsigned int const inval   = fgetc(ifP);
        memset(palmrow + j, inval, incount);
        j += incount;
    }
} 



static void
readScanlineRow(FILE *          const ifP,
                unsigned char * const palmrow,
                unsigned char * const lastrow,
                unsigned int    const bytesPerRow,
                bool            const firstRow) {

    unsigned int j;
    unsigned int incount, inval;
    unsigned char inbit;

    for (j = 0;  j < bytesPerRow;  j += 8) {
        incount = fgetc(ifP);
        inval   = MIN(bytesPerRow - j, 8);
        
        for (inbit = 0;  inbit < inval;  inbit += 1) {
            /* the first row cannot be compressed */
            if (firstRow || ((incount & (1 << (7 - inbit))) != 0))
                palmrow[j + inbit] = fgetc(ifP);
            else
                palmrow[j + inbit] = lastrow[j + inbit];
        }
    }
    memcpy(lastrow, palmrow, bytesPerRow);
}



static void
readUncompressedRow(FILE *          const ifP,
                    unsigned char * const palmrow,
                    unsigned int    const bytesPerRow) {

    int bytesRead;
    
    bytesRead = fread(palmrow, 1, bytesPerRow, ifP);
    if (bytesRead != bytesPerRow)
        pm_error("Error reading Palm file.  Short read.");
}



static void
readDecompressedRow(FILE *                   const ifP,
                    unsigned char *          const palmrow,
                    unsigned char *          const lastrow,
                    enum palmCompressionType const compressionType,
                    unsigned int             const bytesPerRow,
                    bool                     const firstRow,
                    bool                     const verbose) {

    switch (compressionType) {
    case COMPRESSION_RLE:
        readRleRow(ifP, palmrow, bytesPerRow);
        break;
    case COMPRESSION_SCANLINE:
        readScanlineRow(ifP, palmrow, lastrow, bytesPerRow, firstRow);
        break;
    case COMPRESSION_PACKBITS:
        pm_error("INTERNAL ERROR: impossible value of compressionType");
        break;
    case COMPRESSION_NONE:
        readUncompressedRow(ifP, palmrow, bytesPerRow);
        break;
    }
}



static void
convertRowToPnmDirect(const unsigned char * const palmrow,
                      xel *                 const xelrow,
                      unsigned int          const cols,
                      xelval                const maxval,
                      unsigned int *        const seen) {

    /* There's a problem with this.  Take the Palm 16-bit
       direct color.  That's 5 bits for the red, 6 for the
       green, and 5 for the blue.  So what should the MAXVAL
       be?  I decided to use 255 (8 bits) for everything,
       since that's the theoretical max of the number of bits
       in any one color, according to Palm.  So the Palm color
       0xFFFF (white) would be red=0x1F, green=0x3F, and
       blue=0x1F.  How do we promote those colors?  Simple
       shift would give us R=248,G=252,B=248; which is
       slightly green.  Hardly seems right.
       
       So I've perverted the math a bit.  Each color value is
       multiplied by 255, then divided by either 31 (red or
       blue) or 63 (green).  That's the right way to do it
       anyway.  
    */

    const unsigned char *inbyte;
    unsigned int j;
    
    for (inbyte = palmrow, j = 0;  j < cols;  ++j) {
        unsigned int inval;
        inval = *inbyte++ << 8;
        inval |= *inbyte++;
        
        if (seen)
            ++seen[inval];
        
        PPM_ASSIGN(xelrow[j], 
                   (((inval >> 11) & 0x1F) * maxval) / 0x1F, 
                   (((inval >>  5) & 0x3F) * maxval) / 0x3F, 
                   (((inval >>  0) & 0x1F) * maxval) / 0x1F
            );
    }
}



static void
convertRowToPnmNotDirect(const unsigned char * const palmrow,
                         xel *                 const xelrow,
                         unsigned int          const cols,
                         Colormap              const colormap,
                         xelval *              const graymap,
                         unsigned int *        const seen,
                         unsigned int          const pixelSize) {

    unsigned int const mask = (1 << pixelSize) - 1;

    const unsigned char *inbyte;
    unsigned int inbit;
    unsigned int j;
    
    inbit = 8 - pixelSize;
    inbyte = palmrow;
    for (j = 0; j < cols; ++j) {
        short const color = ((*inbyte) & (mask << inbit)) >> inbit;
        if (seen)
            ++seen[color];
        
        if (colormap) {
            Color_s const color2      = color << 24;
            Color   const actualColor = (bsearch (&color2,
                                                  colormap->color_entries, 
                                                  colormap->ncolors,
                                                  sizeof(color2), 
                                                  palmcolor_compare_indices));
            PPM_ASSIGN(xelrow[j], 
                       (*actualColor >> 16) & 0xFF, 
                       (*actualColor >>  8) & 0xFF, 
                       (*actualColor >>  0) & 0xFF);
        } else
            PNM_ASSIGN1(xelrow[j], graymap[color]);
        
        if (!inbit) {
            ++inbyte;
            inbit = 8 - pixelSize;
        } else
            inbit -= pixelSize;
    }
}



static void
writePnm(struct palmHeader const palmHeader,
         FILE *            const ifP,
         Colormap          const colormap,
         xelval *          const graymap,
         unsigned int      const nColors,
         int               const format,
         xelval            const maxval,
         unsigned int **   const seenP,
         bool              const verbose) {

    int const cols = palmHeader.cols;
    int const rows = palmHeader.rows;

    unsigned char * palmrow;
    unsigned char * lastrow;
    xel *           xelrow;
    unsigned int *  seen;
    unsigned int    row;
    
    pnm_writepnminit(stdout, cols, rows, maxval, format, 0);
    xelrow = pnm_allocrow(cols);

    /* Read the picture data, one row at a time */
    MALLOCARRAY_NOFAIL(palmrow, palmHeader.bytesPerRow);
    MALLOCARRAY_NOFAIL(lastrow, palmHeader.bytesPerRow); 
    
    if (seenP) {
        createHistogram(nColors, &seen);
        *seenP = seen;
    } else
        seen = NULL;

    if (palmHeader.compressionType != COMPRESSION_NONE) {
        if (palmHeader.version < 3) {
            short compressedDataSize16;
            pm_readbigshort(ifP, &compressedDataSize16);
        } else {
            long compressedDataSize32;
            pm_readbiglong(ifP, &compressedDataSize32);
        }
    }

    for (row = 0; row < rows; ++row) {
        readDecompressedRow(ifP, palmrow, lastrow, 
                            palmHeader.compressionType, 
                            palmHeader.bytesPerRow,
                            row == 0, verbose);

        if (palmHeader.flags & PALM_DIRECT_COLOR) {
            assert(palmHeader.pixelSize == 16);
            convertRowToPnmDirect(palmrow, xelrow, cols, maxval, seen);
        } else
            convertRowToPnmNotDirect(palmrow, xelrow, cols, colormap, graymap,
                                     seen, palmHeader.pixelSize);

        pnm_writepnmrow(stdout, xelrow, cols, maxval, format, 0);
    }
    free(lastrow);
    free(palmrow);
    pnm_freerow(xelrow);
}



static void
showHistogram(unsigned int * const seen,
              Colormap       const colormap,
              const xelval * const graymap,
              unsigned int   const ncolors) {

    unsigned int colorIndex;
    
    for (colorIndex = 0;  colorIndex < ncolors; ++colorIndex) {
        if (!colormap)
            pm_message("%.3d -> %.3d:  %d", 
                       colorIndex, graymap[colorIndex], seen[colorIndex]);
        else {
            Color_s const color = colorIndex << 24;
            Color const actualColor = (bsearch(&color,
                                               colormap->color_entries, 
                                               colormap->ncolors,
                                               sizeof(color), 
                                               palmcolor_compare_indices));
            if (actualColor)
                pm_message("%.3d -> %ld,%ld,%ld:  %d", colorIndex,
                           (*actualColor >> 16) & 0xFF,
                           (*actualColor >> 8) & 0xFF,
                           (*actualColor & 0xFF), seen[colorIndex]);
        }
    }
}



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

    struct cmdlineInfo cmdline;

    FILE* ifP;
    struct palmHeader palmHeader;
    Colormap colormap;
    struct directColorInfo directColorInfo;
    int format;
    xelval maxval;
    unsigned int nColors;

    /* Parse default params */
    pnm_init(&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    ifP = pm_openr(cmdline.inputFilespec);

    readHeader(ifP, cmdline.rendition, &palmHeader);
    
    if (cmdline.verbose)
        reportPalmHeader(palmHeader);

    if (palmHeader.compressionType == COMPRESSION_PACKBITS)
        pm_error("The image is compression with the packbits method.  "
                 "This program does not know how to read that.");
    if ((palmHeader.flags & PALM_DIRECT_COLOR) &&
        palmHeader.pixelSize != 16)
        pm_error("The image is of the direct color type, but has %u "
                 "bits per pixel.  The only kind of direct color images "
                 "this program understands are 16 bit ones.", 
                 palmHeader.pixelSize);

    determineOutputFormat(palmHeader, &format, &maxval);
    
    getColorInfo(palmHeader, ifP, &colormap, &nColors, &directColorInfo);

    if (cmdline.transparent)
        doTransparent(palmHeader.flags, palmHeader.transparentIndex, 
                      palmHeader.pixelSize, colormap, directColorInfo);
    else {
        unsigned int * seen;
        xelval * graymap;

        graymap = createGraymap(nColors, 0, maxval);

        writePnm(palmHeader, ifP, colormap, graymap, nColors, format, maxval, 
                 cmdline.showhist ? &seen : NULL,
                 cmdline.verbose);
        
        if (cmdline.showhist)
            showHistogram(seen, colormap, graymap, nColors);
        
        free(graymap);
    }
    pm_close(ifP);

    return 0;
}

