/* pnmarith.c - perform arithmetic on two portable anymaps
**
** slightly modified by Marcel Wijkstra <wijkstra@fwi.uva.nl>
**
**
** Copyright (C) 1989, 1991 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.
*/

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

#include "pam.h"
#include "shhopt.h"

enum function {FN_ADD, FN_SUBTRACT, FN_MULTIPLY, FN_DIFFERENCE,
               FN_MINIMUM, FN_MAXIMUM, FN_MEAN, FN_COMPARE,
               FN_AND, FN_OR, FN_NAND, FN_NOR, FN_XOR,
               FN_SHIFTLEFT, FN_SHIFTRIGHT
              };

struct cmdlineInfo {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    const char *input1Filespec;  
    const char *input2Filespec;  
    enum function function;
};



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 = malloc(100*sizeof(optEntry));
        /* Instructions to OptParseOptions2 on how to parse our options.
         */
    optStruct3 opt;

    unsigned int option_def_index;
    
    unsigned int addSpec, subtractSpec, multiplySpec, differenceSpec,
        minimumSpec, maximumSpec, meanSpec, compareSpec,
    andSpec, orSpec, nandSpec, norSpec, xorSpec,
    shiftleftSpec, shiftrightSpec;

    option_def_index = 0;   /* incremented by OPTENTRY */
    OPTENT3(0, "add",         OPT_FLAG,   NULL, &addSpec,        0);
    OPTENT3(0, "subtract",    OPT_FLAG,   NULL, &subtractSpec,   0);
    OPTENT3(0, "multiply",    OPT_FLAG,   NULL, &multiplySpec,   0);
    OPTENT3(0, "difference",  OPT_FLAG,   NULL, &differenceSpec, 0);
    OPTENT3(0, "minimum",     OPT_FLAG,   NULL, &minimumSpec,    0);
    OPTENT3(0, "maximum",     OPT_FLAG,   NULL, &maximumSpec,    0);
    OPTENT3(0, "mean",        OPT_FLAG,   NULL, &meanSpec,       0);
    OPTENT3(0, "compare",     OPT_FLAG,   NULL, &compareSpec,    0);
    OPTENT3(0, "and",         OPT_FLAG,   NULL, &andSpec,        0);
    OPTENT3(0, "or",          OPT_FLAG,   NULL, &orSpec,         0);
    OPTENT3(0, "nand",        OPT_FLAG,   NULL, &nandSpec,       0);
    OPTENT3(0, "nor",         OPT_FLAG,   NULL, &norSpec,        0);
    OPTENT3(0, "xor",         OPT_FLAG,   NULL, &xorSpec,        0);
    OPTENT3(0, "shiftleft",   OPT_FLAG,   NULL, &shiftleftSpec,  0);
    OPTENT3(0, "shiftright",  OPT_FLAG,   NULL, &shiftrightSpec, 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 *cmdlineP and others. */

    if (addSpec + subtractSpec + multiplySpec + differenceSpec +
        minimumSpec + maximumSpec + meanSpec + compareSpec +
        andSpec + orSpec + nandSpec + norSpec + xorSpec +
        shiftleftSpec + shiftrightSpec > 1)
        pm_error("You may specify only one function");

    if (argc-1 != 2)
        pm_error("You must specify two arguments:  the files which are "
                 "the operands of the "
                 "dyadic function.  You specified %d", argc-1);
    else {
        cmdlineP->input1Filespec = argv[1];
        cmdlineP->input2Filespec = argv[2];
    }

    if (addSpec)
        cmdlineP->function = FN_ADD;
    else if (subtractSpec)
        cmdlineP->function = FN_SUBTRACT;
    else if (multiplySpec)
        cmdlineP->function = FN_MULTIPLY;
    else if (differenceSpec)
        cmdlineP->function = FN_DIFFERENCE;
    else if (minimumSpec)
        cmdlineP->function = FN_MINIMUM;
    else if (maximumSpec)
        cmdlineP->function = FN_MAXIMUM;
    else if (meanSpec)
        cmdlineP->function = FN_MEAN;
    else if (compareSpec)
        cmdlineP->function = FN_COMPARE;
    else if (andSpec)
        cmdlineP->function = FN_AND;
    else if (orSpec)
        cmdlineP->function = FN_OR;
    else if (nandSpec)
        cmdlineP->function = FN_NAND;
    else if (norSpec)
        cmdlineP->function = FN_NOR;
    else if (xorSpec)
        cmdlineP->function = FN_XOR;
    else if (shiftleftSpec)
        cmdlineP->function = FN_SHIFTLEFT;
    else if (shiftrightSpec)
        cmdlineP->function = FN_SHIFTRIGHT;
    else
        pm_error("You must specify a function (e.g. '-add')");
}        



enum category {CATEGORY_FRACTIONAL_ARITH, CATEGORY_BITSTRING, CATEGORY_SHIFT};

static enum category
functionCategory(enum function const function) {

    enum category retval;
    
    switch (function) {
    case FN_ADD:
    case FN_SUBTRACT:
    case FN_MULTIPLY:
    case FN_DIFFERENCE:
    case FN_MINIMUM:
    case FN_MAXIMUM:
    case FN_MEAN:
    case FN_COMPARE:
        retval = CATEGORY_FRACTIONAL_ARITH;
        break;
    case FN_AND:
    case FN_OR:
    case FN_NAND:
    case FN_NOR:
    case FN_XOR:
        retval = CATEGORY_BITSTRING;
        break;
    case FN_SHIFTLEFT:
    case FN_SHIFTRIGHT:
        retval = CATEGORY_SHIFT;
        break;
    }
    return retval;
}


static void
computeOutputType(struct pam *  const outpamP,
                  struct pam    const inpam1,
                  struct pam    const inpam2,
                  enum function const function) {

    outpamP->size        = sizeof(struct pam);
    outpamP->len         = PAM_STRUCT_SIZE(tuple_type);
    outpamP->file        = stdout;
    outpamP->format      = MAX(inpam1.format, inpam2.format);
    outpamP->plainformat = FALSE;
    outpamP->height      = inpam1.height;
    outpamP->width       = inpam1.width;
    outpamP->depth       = MAX(inpam1.depth, inpam2.depth);

    switch (functionCategory(function)) {    
    case CATEGORY_FRACTIONAL_ARITH:
        if (function == FN_COMPARE)
            outpamP->maxval = 2;
        else
            outpamP->maxval = MAX(inpam1.maxval, inpam2.maxval);
        break;
    case CATEGORY_BITSTRING:
        if (inpam2.maxval != inpam1.maxval)
            pm_error("For a bit string operation, the maxvals of the "
                     "left and right image must be the same.  You have "
                     "left=%u and right=%u", 
                     (unsigned)inpam1.maxval, (unsigned)inpam2.maxval);

        if (pm_bitstomaxval(pm_maxvaltobits(inpam1.maxval)) != inpam1.maxval)
            pm_error("For a bit string operation, the maxvals of the inputs "
                     "must be a full binary count, i.e. a power of two "
                     "minus one such as 0xff.  You have 0x%x",
                     (unsigned)inpam1.maxval);

        outpamP->maxval = inpam1.maxval;
        break;
    case CATEGORY_SHIFT:
        if (pm_bitstomaxval(pm_maxvaltobits(inpam1.maxval)) != inpam1.maxval)
            pm_error("For a bit shift operation, the maxval of the left "
                     "input image "
                     "must be a full binary count, i.e. a power of two "
                     "minus one such as 0xff.  You have 0x%x",
                     (unsigned)inpam1.maxval);
        outpamP->maxval = inpam1.maxval;
    }
    outpamP->bytes_per_sample = (pm_maxvaltobits(outpamP->maxval)+7)/8;
    strcpy(outpamP->tuple_type, inpam1.tuple_type);
}



static sample
applyFunction(enum function const function,
              sample        const leftArg,
              sample        const rightArg,
              sample        const resultMaxval) {

    sample retval;
    double resultArith;
    unsigned int resultBit;

    switch (function) {
    case FN_ADD:
        resultArith = MIN(resultMaxval, leftArg + rightArg);
        break;
    case FN_SUBTRACT:
        resultArith = MAX(0, (int)leftArg - (int)rightArg);
        break;
    case FN_MULTIPLY:
        resultArith = leftArg * rightArg / resultMaxval;
        break;
    case FN_DIFFERENCE:
        resultArith = leftArg > rightArg ? 
            leftArg - rightArg : rightArg - leftArg;
        break;
    case FN_MINIMUM:
        resultArith = MIN(leftArg, rightArg);
        break;
    case FN_MAXIMUM:
        resultArith = MAX(leftArg, rightArg);
        break;
    case FN_MEAN:
        resultArith = (leftArg + rightArg) / 2.0;
        break;
    case FN_COMPARE:
        resultArith = 
            leftArg > rightArg ? 2 : leftArg < rightArg ? 0 : 1;
        break;
    case FN_AND:
        resultBit = leftArg & rightArg;
        break;
    case FN_OR:
        resultBit = leftArg | rightArg;
        break;
    case FN_NAND:
        resultBit = ~(leftArg & rightArg) & resultMaxval;
        break;
    case FN_NOR:
        resultBit = ~(leftArg | rightArg) & resultMaxval;
        break;
    case FN_XOR:
        resultBit = leftArg ^ rightArg;
        break;
    case FN_SHIFTLEFT:
        resultBit = (leftArg << rightArg) & resultMaxval;
        break;
    case FN_SHIFTRIGHT:
        resultBit = leftArg >> rightArg;
        break;
    }
    if (functionCategory(function) == CATEGORY_FRACTIONAL_ARITH)
        retval = (sample) (resultArith + 0.5);
    else
        retval = resultBit;

    return retval;
}



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

    struct cmdlineInfo cmdline;
    struct pam inpam1;
    struct pam inpam2;
    struct pam outpam;
    FILE* if1P;
    FILE* if2P;
    tuple* tuplerow1;
    tuple* tuplerow2;
    tuple* tuplerowOut;
    unsigned int row;
    
    pnm_init(&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    if1P = pm_openr(cmdline.input1Filespec);
    if2P = pm_openr(cmdline.input2Filespec);

    pnm_readpaminit(if1P, &inpam1, PAM_STRUCT_SIZE(tuple_type));
    pnm_readpaminit(if2P, &inpam2, PAM_STRUCT_SIZE(tuple_type));

    if (inpam1.width != inpam2.width || inpam1.height != inpam2.height)
        pm_error("The two images must be the same width and height.  "
                 "The first is %dx%dx%d, but the second is %dx%dx%d",
                 inpam1.width, inpam1.height, inpam1.depth,
                 inpam2.width, inpam2.height, inpam2.depth);

    if (inpam1.depth != 1 && inpam2.depth != 1 && inpam1.depth != inpam2.depth)
        pm_error("The two images must have the same depth or one of them "
                 "must have depth 1.  The first has depth %d and the second "
                 "has depth %d", inpam1.depth, inpam2.depth);

    computeOutputType(&outpam, inpam1, inpam2, cmdline.function);

    tuplerow1 = pnm_allocpamrow(&inpam1);
    tuplerow2 = pnm_allocpamrow(&inpam2);
    tuplerowOut = pnm_allocpamrow(&outpam);

    pnm_writepaminit(&outpam);

    for (row = 0; row < outpam.height; ++row) {
        unsigned int col;
        pnm_readpamrow(&inpam1, tuplerow1);
        pnm_readpamrow(&inpam2, tuplerow2);
        
        if (outpam.maxval != inpam1.maxval)
            pnm_scaletuplerow(&inpam1, tuplerow1, tuplerow1, outpam.maxval);
        if (outpam.maxval != inpam2.maxval)
            pnm_scaletuplerow(&inpam2, tuplerow2, tuplerow2, outpam.maxval);

        for (col = 0; col < outpam.width; ++col) {
            unsigned int outplane;
            
            for (outplane = 0; outplane < outpam.depth; ++outplane) {
                unsigned int const plane1 = MIN(outplane, inpam1.depth-1);
                unsigned int const plane2 = MIN(outplane, inpam2.depth-1);

                tuplerowOut[col][outplane] = 
                    applyFunction(cmdline.function, 
                                  tuplerow1[col][plane1], 
                                  tuplerow2[col][plane2],
                                  outpam.maxval);

                assert(tuplerowOut[col][outplane] >= 0);
                assert(tuplerowOut[col][outplane] <= outpam.maxval);
            }
        }
        pnm_writepamrow(&outpam, tuplerowOut);
    }

    pm_close(if1P);
    pm_close(if2P);

    pnm_freepamrow(tuplerow1);
    pnm_freepamrow(tuplerow2);
    pnm_freepamrow(tuplerowOut);
    
    return 0;
}
