/* pamtotga.c - read a portable pixmap and produce a TrueVision Targa file
**
** Copyright (C) 1989, 1991 by Mark Shand and 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.
*/

#define _BSD_SOURCE  /* Make sure string.h contains strdup() */
#include <string.h>

#include "pam.h"
#include "pammap.h"
#include "shhopt.h"
#include "mallocvar.h"
#include "tga.h"

/* Max number of colors allowed for colormapped output. */
#define MAXCOLORS 256

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 */
    char *outName;
    enum TGAbaseImageType imgType;
    bool defaultFormat;
    unsigned int norle;
};



static void
parseCommandLine(int argc, char ** argv,
                 struct cmdlineInfo * const cmdlineP) {
/*----------------------------------------------------------------------------
   Parse the program arguments (given by argc and argv) into a form
   the program can deal with more easily -- a cmdline_info structure.
   If the syntax is invalid, issue a message and exit the program via
   pm_error().

   Note that the file spec array we return is stored in the storage that
   was passed to us as the argv array.
-----------------------------------------------------------------------------*/
    optStruct3 opt;  /* set by OPTENT3 */
    optEntry *option_def = malloc(100*sizeof(optEntry));
    unsigned int option_def_index;

    unsigned int outNameSpec;
    unsigned int cmap, mono, rgb;

    option_def_index = 0;   /* incremented by OPTENT3 */
    OPTENT3(0,   "name",       OPT_STRING, 
            &cmdlineP->outName, &outNameSpec, 0);
    OPTENT3(0,   "cmap",       OPT_FLAG, 
            NULL, &cmap, 0);
    OPTENT3(0,   "mono",       OPT_FLAG, 
            NULL, &mono, 0);
    OPTENT3(0,   "rgb",        OPT_FLAG, 
            NULL, &rgb, 0);
    OPTENT3(0,   "norle",      OPT_FLAG, 
            NULL, &cmdlineP->norle, 0);
    
    opt.opt_table = option_def;
    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */

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

    if (cmap + mono + rgb > 1)
        pm_error("You may specify only one of -cmap, -mono, and -rgb.");

    if (cmap + mono + rgb == 0)
        cmdlineP->defaultFormat = TRUE;
    else {
        cmdlineP->defaultFormat = FALSE;
    
        if (cmap)
            cmdlineP->imgType = TGA_MAP_TYPE;
        else if (mono)
            cmdlineP->imgType = TGA_MONO_TYPE;
        else if (rgb)
            cmdlineP->imgType = TGA_RGB_TYPE;
    }

    if (!outNameSpec)
        cmdlineP->outName = NULL;
    
    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
writeTgaHeader(struct ImageHeader const tgaHeader) {

    unsigned char flags;

    putchar( tgaHeader.IDLength );
    putchar( tgaHeader.CoMapType );
    putchar( tgaHeader.ImgType );
    putchar( tgaHeader.Index_lo );
    putchar( tgaHeader.Index_hi );
    putchar( tgaHeader.Length_lo );
    putchar( tgaHeader.Length_hi );
    putchar( tgaHeader.CoSize );
    putchar( tgaHeader.X_org_lo );
    putchar( tgaHeader.X_org_hi );
    putchar( tgaHeader.Y_org_lo );
    putchar( tgaHeader.Y_org_hi );
    putchar( tgaHeader.Width_lo );
    putchar( tgaHeader.Width_hi );
    putchar( tgaHeader.Height_lo );
    putchar( tgaHeader.Height_hi );
    putchar( tgaHeader.PixelSize );
    flags = ( tgaHeader.AttBits & 0xf ) | 
        ( ( tgaHeader.Rsrvd & 0x1 ) << 4 ) |
        ( ( tgaHeader.OrgBit & 0x1 ) << 5 ) | 
        ( ( tgaHeader.OrgBit & 0x3 ) << 6 );
    putchar( flags );

    if ( tgaHeader.IDLength > 0 )
        fwrite( tgaHeader.id, 1, (int) tgaHeader.IDLength, stdout );
}
    


static void
putPixel(struct pam *          const pamP,
         tuple                 const tuple, 
         enum TGAbaseImageType const imgType, 
         tuplehash             const cht) {

    if (imgType == TGA_MAP_TYPE) {
        int retval;
        int found;

        pnm_lookuptuple(pamP, cht, tuple, &found, &retval);
        if (!found)
            pm_error("Internal error: color not found in map that was "
                     "generated from all the colors in the image");
        putchar(retval);
    } else {
        if (imgType == TGA_RGB_TYPE && pamP->depth < 3) {
            /* Make RGB pixel out of a single input plane */
            unsigned int plane;
            
            for (plane = 0; plane < pamP->depth; ++plane) 
                putchar(pnm_scalesample(tuple[0], 
                                        pamP->maxval, TGA_MAXVAL));
        } else if (imgType == TGA_MONO_TYPE)
            putchar(pnm_scalesample(tuple[0], 
                                    pamP->maxval, TGA_MAXVAL));
        else {
            putchar(pnm_scalesample(tuple[PAM_BLU_PLANE], 
                                    pamP->maxval, TGA_MAXVAL));
            putchar(pnm_scalesample(tuple[PAM_GRN_PLANE], 
                                    pamP->maxval, TGA_MAXVAL));
            putchar(pnm_scalesample(tuple[PAM_RED_PLANE], 
                                    pamP->maxval, TGA_MAXVAL));
            if (pamP->depth > 3)
                putchar(pnm_scalesample(tuple[PAM_TRN_PLANE], 
                                        pamP->maxval, TGA_MAXVAL));
        }
    }
}



static void
putMapEntry(struct pam * const pamP, 
            tuple        const value, 
            int          const size) {

    if (size == 15 || size == 16) {
        /* 5 bits each of red, green, and blue.  Watch for byte order */

        tuple const tuple31 = pnm_allocpamtuple(pamP);

        pnm_scaletuple(pamP, tuple31, value, 31);
        {
            int const mapentry = 
                tuple31[PAM_BLU_PLANE] << 0 |
                tuple31[PAM_GRN_PLANE] << 5 |
                tuple31[PAM_RED_PLANE] << 10;
            
            putchar(mapentry % 256);
            putchar(mapentry / 256);
        }
        pnm_freepamtuple(tuple31);
    } else if (size == 8)
        putchar(pnm_scalesample(value[0], 
                                pamP->maxval, TGA_MAXVAL));
    else {
        /* Must be 24 or 32 */
        putchar(pnm_scalesample(value[PAM_BLU_PLANE], 
                                pamP->maxval, TGA_MAXVAL));
        putchar(pnm_scalesample(value[PAM_GRN_PLANE], 
                                pamP->maxval, TGA_MAXVAL));
        putchar(pnm_scalesample(value[PAM_RED_PLANE], 
                                    pamP->maxval, TGA_MAXVAL));
        if (size == 32)
            putchar(pnm_scalesample(value[PAM_TRN_PLANE], 
                                    pamP->maxval, TGA_MAXVAL));
    }
}



static void
computeRunlengths(struct pam * const pamP, 
                   tuple *      const tuplerow, 
                   int *        const runlength) {

    int col, start;

    /* Initialize all run lengths to 0.  (This is just an error check.) */
    for (col = 0; col < pamP->width; ++col)
        runlength[col] = 0;
    
    /* Find runs of identical pixels. */
    for ( col = 0; col < pamP->width; ) {
        start = col;
        do {
            ++col;
        } while ( col < pamP->width &&
                  col - start < 128 &&
                  pnm_tupleequal(pamP, tuplerow[col], tuplerow[start]));
        runlength[start] = col - start;
    }
    
    /* Now look for runs of length-1 runs, and turn them into negative runs. */
    for (col = 0; col < pamP->width; ) {
        if (runlength[col] == 1) {
            start = col;
            while (col < pamP->width &&
                   col - start < 128 &&
                   runlength[col] == 1 ) {
                runlength[col] = 0;
                ++col;
            }
            runlength[start] = - ( col - start );
        } else
            col += runlength[col];
    }
}



static void
computeOutName(struct cmdlineInfo const cmdline, 
               const char ** const outNameP) {
    
    char * workarea;

    if (cmdline.outName)
        workarea = strdup(cmdline.outName);
    else if (strcmp(cmdline.inputFilespec, "-") == 0)
        workarea = NULL;
    else {
        char * cp;
        workarea = strdup(cmdline.inputFilespec);
        cp = strchr(workarea, '.');
        if (cp != NULL)
        	*cp = '\0';	/* remove extension */
    }
    
    if (workarea == NULL)
        *outNameP = NULL;
    else {
        /* Truncate the name to fit TGA specs */
        if (strlen(workarea) > IMAGEIDFIELDMAXSIZE)
            workarea[IMAGEIDFIELDMAXSIZE] = '\0';
        *outNameP = workarea;
    }
}



static void
validateTupleType(struct pam * const pamP) {

    if (strcmp(pamP->tuple_type, "RGBA") == 0) {
        if (pamP->depth < 4)
            pm_error("Invalid depth for tuple type RGBA.  "
                     "Should have at least 4 planes, but has %d.", 
                     pamP->depth);
    } else if (strcmp(pamP->tuple_type, "RGB") == 0) {
        if (pamP->depth < 3)
            pm_error("Invalid depth for tuple type RGB.  "
                     "Should have at least 3 planes, but has %d.", 
                     pamP->depth);
    } else if (strcmp(pamP->tuple_type, "GRAYSCALE") == 0) {
    } else if (strcmp(pamP->tuple_type, "BLACKANDWHITE") == 0) {
    } else 
        pm_error("Invalid type of input.  PAM tuple type is '%s'.  "
                 "This programs understands only RGBA, RGB, GRAYSCALE, "
                 "and BLACKANDWHITE.", pamP->tuple_type);
}



static void
computeImageType_cht(struct pam *            const pamP,
                     struct cmdlineInfo      const cmdline, 
                     tuple **                const tuples,
                     enum TGAbaseImageType * const baseImgTypeP,
                     tupletable *            const chvP,
                     tuplehash *             const chtP, 
                     int *                   const ncolorsP) {

    unsigned int ncolors;
    enum TGAbaseImageType baseImgType;

    validateTupleType(pamP);

    if (cmdline.defaultFormat) {
        /* default the image type */
        if (pamP->depth > 1) {
            pm_message("computing colormap...");
            *chvP = 
                pnm_computetuplefreqtable(pamP, tuples, MAXCOLORS, &ncolors);
            if (*chvP == NULL) {
                pm_message("Too many colors for colormapped TGA.  Doing RGB.");
                baseImgType = TGA_RGB_TYPE;
            } else 
                baseImgType = TGA_MAP_TYPE;
        } else
            baseImgType = TGA_MONO_TYPE;
    } else {
        baseImgType = cmdline.imgType;

        if (baseImgType == TGA_MAP_TYPE) {
            pm_message("computing colormap...");
            *chvP = 
                pnm_computetuplefreqtable(pamP, tuples, MAXCOLORS, &ncolors);
            if (*chvP == NULL) 
                pm_error("Too many colors for colormapped TGA.  "
                         "Use 'pnmquant %d' to reduce the number of colors.", 
                         MAXCOLORS);
        }
        if (baseImgType == TGA_MONO_TYPE && pamP->depth > 1)
            pm_error("For Mono TGA output, input must be "
                     "GRAYSCALE or BLACKANDWHITE PAM or PBM or PGM");
    }
    
    if (baseImgType == TGA_MAP_TYPE) {
        pm_message("%d colors found.", ncolors);
        /* Make a hash table for fast color lookup. */
        *chtP = pnm_computetupletablehash(pamP, *chvP, ncolors);
    } else
        *chtP = NULL;

    *baseImgTypeP = baseImgType;
    *ncolorsP = ncolors;
}



static void
computeTgaHeader(struct pam *          const pamP,
                 enum TGAbaseImageType const baseImgType,
                 bool                  const rle, 
                 int                   const ncolors,
                 unsigned char         const orgBit,
                 const char *          const id,
                 struct ImageHeader *  const tgaHeaderP) {

    if (rle) {
        switch (baseImgType ) {
        case TGA_MONO_TYPE: tgaHeaderP->ImgType = TGA_RLEMono;          break;
        case TGA_MAP_TYPE:  tgaHeaderP->ImgType = TGA_RLEMap;           break;
        case TGA_RGB_TYPE:  tgaHeaderP->ImgType = TGA_RLERGB;           break;
        }
    } else {
        switch(baseImgType) {
        case TGA_MONO_TYPE: tgaHeaderP->ImgType = TGA_Mono;          break;
        case TGA_MAP_TYPE:  tgaHeaderP->ImgType = TGA_Map;           break;
        case TGA_RGB_TYPE:  tgaHeaderP->ImgType = TGA_RGB;           break;
        }
    }
    
    if (id)
        tgaHeaderP->IDLength = strlen(id);
    else
        tgaHeaderP->IDLength = 0;
    tgaHeaderP->Index_lo = 0;
    tgaHeaderP->Index_hi = 0;
    if (baseImgType == TGA_MAP_TYPE) {
        tgaHeaderP->CoMapType = 1;
        tgaHeaderP->Length_lo = ncolors % 256;
        tgaHeaderP->Length_hi = ncolors / 256;
        tgaHeaderP->CoSize = 8 * pamP->depth;
    } else {
        tgaHeaderP->CoMapType = 0;
        tgaHeaderP->Length_lo = 0;
        tgaHeaderP->Length_hi = 0;
        tgaHeaderP->CoSize = 0;
    }
    switch (baseImgType) {
    case TGA_MAP_TYPE:
        tgaHeaderP->PixelSize = 8;
        break;
    case TGA_RGB_TYPE:
        tgaHeaderP->PixelSize = 8 * MAX(3, pamP->depth);
        break;
    case TGA_MONO_TYPE:
        tgaHeaderP->PixelSize = 8;
    }
    tgaHeaderP->X_org_lo = tgaHeaderP->X_org_hi = 0;
    tgaHeaderP->Y_org_lo = tgaHeaderP->Y_org_hi = 0;
    tgaHeaderP->Width_lo = pamP->width % 256;
    tgaHeaderP->Width_hi = pamP->width / 256;
    tgaHeaderP->Height_lo = pamP->height % 256;
    tgaHeaderP->Height_hi = pamP->height / 256;
    tgaHeaderP->AttBits = 0;
    tgaHeaderP->Rsrvd = 0;
    tgaHeaderP->IntrLve = 0;
    tgaHeaderP->OrgBit = orgBit;
}



static void 
writeTgaRaster(struct pam *          const pamP,
               tuple **              const tuples, 
               tuplehash             const cht,
               enum TGAbaseImageType const imgType,
               bool                  const rle,
               unsigned char         const orgBit) {

    int* runlength;  /* malloc'ed */
    int row;

    if (rle)
        MALLOCARRAY(runlength, pamP->width);

    for (row = 0; row < pamP->height; ++row) {
        int realrow;
        realrow = (orgBit != 0) ? row : pamP->height - row - 1;
        if (rle) {
            int col;
            computeRunlengths(pamP, tuples[realrow], runlength);
            for (col = 0; col < pamP->width; ) {
                if (runlength[col] > 0) {
                    putchar(0x80 + runlength[col] - 1);
                    putPixel(pamP, tuples[realrow][col], imgType, cht);
                    col += runlength[col];
                } else if (runlength[col] < 0) {
                    int i;
                    putchar(-runlength[col] - 1);
                    for (i = 0; i < -runlength[col]; ++i)
                        putPixel(pamP, tuples[realrow][col+i], imgType, cht);
                    col += -runlength[col];
                } else
                    pm_error("Internal error: zero run length");
            }
        } else {
            int col;
            for (col = 0; col < pamP->width; ++col)
                putPixel(pamP, tuples[realrow][col], imgType, cht);
        }
    }
    if (rle)
        free(runlength);
}



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

    struct cmdlineInfo cmdline;
    FILE * ifP;
    tuple ** tuples;
    struct pam pam;
    int ncolors;
    tupletable chv;
    tuplehash cht;
    struct ImageHeader tgaHeader;
    enum TGAbaseImageType baseImgType;
    const char *outName;

    pnm_init( &argc, argv );

    parseCommandLine(argc, argv, &cmdline);

    ifP = pm_openr(cmdline.inputFilespec);

    computeOutName(cmdline, &outName);

    tuples = pnm_readpam(ifP, &pam, sizeof(pam));
    pm_close(ifP);

    computeImageType_cht(&pam, cmdline, tuples, 
                         &baseImgType, &chv, &cht, &ncolors);

    /* Do the Targa header */
    computeTgaHeader(&pam, baseImgType, !cmdline.norle,
                     ncolors, 0, outName, &tgaHeader);
    writeTgaHeader(tgaHeader);
    
    if (baseImgType == TGA_MAP_TYPE) {
        /* Write out the Targa colormap. */
        int i;
        for (i = 0; i < ncolors; ++i)
            putMapEntry(&pam, chv[i]->tuple, tgaHeader.CoSize);
    }

    writeTgaRaster(&pam, tuples, cht, baseImgType, !cmdline.norle, 0);

    if (cht)
        pnm_destroytuplehash(cht);
    if (chv)
        pnm_freetupletable(&pam, chv);

    pnm_freepamarray(tuples, &pam);

    exit(0);
}
