/* ----------------------------------------------------------------------
 *
 * Replace every pixel in an image with one of equal luminance
 *
 * By Scott Pakin <scott+pbm@pakin.org>
 *
 * ----------------------------------------------------------------------
 *
 * Copyright (C) 2010 Scott Pakin <scott+pbm@pakin.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 *
 * ----------------------------------------------------------------------
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>

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

/* Two numbers less than REAL_EPSILON apart are considered equal. */
#define REAL_EPSILON 0.00001

/* Ensure a number N is no less than A and no greater than B. */
#define CLAMPxy(N, A, B) MAX(MIN((float)(N), (float)(B)), (float)(A))


struct rgbfrac {
    /* This structure represents red, green, and blue, each expressed
       as a fraction from 0.0 to 1.0.
    */
    float rfrac;
    float gfrac;
    float bfrac;
};

struct cmdlineInfo {
    /* This structure represents all of the information the user
       supplied in the command line but in a form that's easy for the
       program to use.
    */
    const char *    inputFileName;  /* '-' if stdin */
    const char *    colorfile;      /* NULL if unspecified */
    struct rgbfrac  color2gray;
        /* colorspace/rmult/gmult/bmult options.  Negative numbers if
           unspecified.
        */
    unsigned int    targetcolorSpec;
    struct rgbfrac  targetcolor;
    unsigned int    randomseed;
    unsigned int    randomseedSpec;
};



static float
rgb2gray(struct rgbfrac * const color2grayP,
         float            const red,
         float            const grn,
         float            const blu) {
    return
        color2grayP->rfrac * red +
        color2grayP->gfrac * grn +
        color2grayP->bfrac * blu;
}



static tuplen *
getColorRow(struct pam  * const pamP,
            tuplen     ** const imageData,
            unsigned int  const row,
            unsigned int  const desiredWidth) {
/*----------------------------------------------------------------------
  Return a row of color data.  If the number of columns is too small,
  repeat the existing columns in tiled fashion.
------------------------------------------------------------------------*/
    unsigned int const imageRow = row % pamP->height;

    static tuplen * oneRow = NULL;

    tuplen * retval;

    if (pamP->width >= desiredWidth)
        retval = imageData[imageRow];
    else {
        unsigned int col;

        if (!oneRow) {
            struct pam widePam;

            widePam = *pamP;
            widePam.width = desiredWidth;

            oneRow = pnm_allocpamrown(&widePam);
        }
        for (col = 0; col < desiredWidth; ++col)
            oneRow[col] = imageData[imageRow][col % pamP->width];
        retval = oneRow;
    }
    return retval;
}



static void
convertRowToGray(struct pam     * const pamP,
                 struct rgbfrac * const color2gray,
                 tuplen         * const tupleRow,
                 samplen        * const grayRow) {
/*----------------------------------------------------------------------
  Convert a row of RGB, grayscale, or black-and-white pixels to a row
  of grayscale values in the range [0, 1].
------------------------------------------------------------------------*/
    switch (pamP->depth) {
    case 1:
    case 2: {
        /* Black-and-white or grayscale */
        unsigned int col;
        for (col = 0; col < pamP->width; ++col)
            grayRow[col] = tupleRow[col][0];
    } break;

    case 3:
    case 4: {
        /* RGB color */
        unsigned int col;
        for (col = 0; col < pamP->width; ++col)
            grayRow[col] = rgb2gray(color2gray,
                                    tupleRow[col][PAM_RED_PLANE],
                                    tupleRow[col][PAM_GRN_PLANE],
                                    tupleRow[col][PAM_BLU_PLANE]);
    } break;

    default:
        pm_error("internal error: unexpected image depth %u", pamP->depth);
        break;
    }
}



static void
explicitlyColorRow(struct pam *   const pamP,
                   tuplen *       const rowData,
                   struct rgbfrac const tint) {

    unsigned int col;

    for (col = 0; col < pamP->width; ++col) {
        rowData[col][PAM_RED_PLANE] = tint.rfrac;
        rowData[col][PAM_GRN_PLANE] = tint.gfrac;
        rowData[col][PAM_BLU_PLANE] = tint.bfrac;
    }
}



static void
randomlyColorRow(struct pam *   const pamP,
                 tuplen *       const rowData) {
/*----------------------------------------------------------------------
  Assign each tuple in a row a random color.
------------------------------------------------------------------------*/
    unsigned int col;

    for (col = 0; col < pamP->width; ++col) {
        rowData[col][PAM_RED_PLANE] = rand() / (float)RAND_MAX;
        rowData[col][PAM_GRN_PLANE] = rand() / (float)RAND_MAX;
        rowData[col][PAM_BLU_PLANE] = rand() / (float)RAND_MAX;
    }
}



static void
recolorRow(struct pam     * const inPamP,
           tuplen         * const inRow,
           struct rgbfrac * const color2grayP,
           tuplen         * const colorRow,
           struct pam     * const outPamP,
           tuplen         * const outRow) {
/*----------------------------------------------------------------------
  Map each tuple in a given row to a random color with the same
  luminance.
------------------------------------------------------------------------*/
    static samplen * grayRow = NULL;

    unsigned int col;

    if (!grayRow)
        MALLOCARRAY_NOFAIL(grayRow, inPamP->width);

    convertRowToGray(inPamP, color2grayP, inRow, grayRow);

    for (col = 0; col < inPamP->width; ++col) {
        float targetgray;
        float givengray;
        float red, grn, blu;

        red = colorRow[col][PAM_RED_PLANE];   /* initial value */
        grn = colorRow[col][PAM_GRN_PLANE];   /* initial value */
        blu = colorRow[col][PAM_BLU_PLANE];   /* initial value */

        targetgray = grayRow[col];
        givengray = rgb2gray(color2grayP, red, grn, blu);

        if (givengray == 0.0) {
            /* Special case for black so we don't divide by zero */
            red = targetgray;
            grn = targetgray;
            blu = targetgray;
        }
        else {
            /* Try simply scaling each channel equally. */
            red *= targetgray / givengray;
            grn *= targetgray / givengray;
            blu *= targetgray / givengray;

            if (red > 1.0 || grn > 1.0 || blu > 1.0) {
                /* Repeatedly raise the level of all non-1.0 channels
                 * until all channels are at 1.0 or we reach our
                 * target gray. */
                red = MIN(red, 1.0);
                grn = MIN(grn, 1.0);
                blu = MIN(blu, 1.0);
                givengray = rgb2gray(color2grayP, red, grn, blu);

                while (fabsf(givengray - targetgray) > REAL_EPSILON) {
                    float increment;
                        /* How much to increase each channel (unscaled
                           amount)
                        */
                    int   subOne = 0;
                        /* Number of channels with sub-1.0 values */

                    /* Tally the number of channels that aren't yet maxed
                       out.
                    */
                    if (red < 1.0)
                        subOne++;
                    if (grn < 1.0)
                        subOne++;
                    if (blu < 1.0)
                        subOne++;

                    /* Stop if we've reached our target or can't increment
                     * any channel any further. */
                    if (subOne == 0)
                        break;

                    /* Brighten each non-maxed channel equally. */
                    increment = (targetgray - givengray) / subOne;
                    if (red < 1.0)
                        red = MIN(red + increment / color2grayP->rfrac, 1.0);
                    if (grn < 1.0)
                        grn = MIN(grn + increment / color2grayP->gfrac, 1.0);
                    if (blu < 1.0)
                        blu = MIN(blu + increment / color2grayP->bfrac, 1.0);

                    /* Prepare to try again. */
                    givengray = rgb2gray(color2grayP, red, grn, blu);
                }
            }
            else
                givengray = rgb2gray(color2grayP, red, grn, blu);
        }

        outRow[col][PAM_RED_PLANE] = red;
        outRow[col][PAM_GRN_PLANE] = grn;
        outRow[col][PAM_BLU_PLANE] = blu;
        if (outPamP->depth == 4)
            outRow[col][PAM_TRN_PLANE] = inRow[col][PAM_TRN_PLANE];
    }
}



static struct rgbfrac
color2GrayFromCsName(const char * const csName) {

    struct rgbfrac retval;

    /* Thanks to
       http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
       for these values.
    */
    if (streq(csName, "ntsc")) {
        /* NTSC RGB with an Illuminant C reference white */
        retval.rfrac = 0.2989164;
        retval.gfrac = 0.5865990;
        retval.bfrac = 0.1144845;
    } else if (streq(csName, "srgb")) {
        /* sRGB with a D65 reference white */
        retval.rfrac = 0.2126729;
        retval.gfrac = 0.7151522;
        retval.bfrac = 0.0721750;
    } else if (streq(csName, "adobe")) {
        /* Adobe RGB (1998) with a D65 reference white */
        retval.rfrac = 0.2973769;
        retval.gfrac = 0.6273491;
        retval.bfrac = 0.0752741;
    } else if (streq(csName, "apple")) {
        /* Apple RGB with a D65 reference white */
        retval.rfrac = 0.2446525;
        retval.gfrac = 0.6720283;
        retval.bfrac = 0.0833192;
    } else if (streq(csName, "cie")) {
        /* CIE with an Illuminant E reference white */
        retval.rfrac = 0.1762044;
        retval.gfrac = 0.8129847;
        retval.bfrac = 0.0108109;
    } else if (streq(csName, "pal")) {
        /* PAL/SECAM with a D65 reference white */
        retval.rfrac = 0.2220379;
        retval.gfrac = 0.7066384;
        retval.bfrac = 0.0713236;
    } else if (streq(csName, "smpte-c")) {
        /* SMPTE-C with a D65 reference white */
        retval.rfrac = 0.2124132;
        retval.gfrac = 0.7010437;
        retval.bfrac = 0.0865432;
    } else if (streq(csName, "wide")) {
        /* Wide gamut with a D50 reference white */
        retval.rfrac = 0.2581874;
        retval.gfrac = 0.7249378;
        retval.bfrac = 0.0168748;
    } else
        pm_error("Unknown color space name \"%s\"", csName);

    return retval;
}



static void
parseCommandLine(int argc, const char ** const argv,
                 struct cmdlineInfo * const cmdlineP ) {

    optEntry     * option_def;
        /* Instructions to OptParseOptions3 on how to parse our options */
    optStruct3     opt;
    unsigned int   option_def_index;
    const char *   colorspaceOpt;
    const char *   targetcolorOpt;
    unsigned int   csSpec, rmultSpec, gmultSpec, bmultSpec,
        colorfileSpec;

    MALLOCARRAY_NOFAIL(option_def, 100);
    option_def_index = 0;          /* Incremented by OPTENTRY */

    OPTENT3(0, "colorspace",   OPT_STRING, &colorspaceOpt, &csSpec, 0);
    OPTENT3(0, "rmult",        OPT_FLOAT,  &cmdlineP->color2gray.rfrac,
            &rmultSpec, 0);
    OPTENT3(0, "gmult",        OPT_FLOAT,  &cmdlineP->color2gray.gfrac,
            &gmultSpec, 0);
    OPTENT3(0, "bmult",        OPT_FLOAT,  &cmdlineP->color2gray.bfrac,
            &bmultSpec, 0);
    OPTENT3(0, "colorfile",    OPT_STRING, &cmdlineP->colorfile,
            &colorfileSpec, 0);
    OPTENT3(0, "targetcolor",  OPT_STRING, &targetcolorOpt,
            &cmdlineP->targetcolorSpec, 0);
    OPTENT3(0,   "randomseed",   OPT_UINT,    &cmdlineP->randomseed,
            &cmdlineP->randomseedSpec,      0);

    opt.opt_table = option_def;
    opt.short_allowed = 0;
    opt.allowNegNum = 0;

    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);

    if (rmultSpec || gmultSpec || bmultSpec) {
        /* If the user explicitly specified RGB multipliers, ensure that
         * (a) he didn't specify --colorspace,
         * (b) he specified all three channels, and
         * (c) the values add up to 1.
         */
        float maxLuminance;

        if (csSpec)
            pm_error("The --colorspace option is mutually exclusive with "
                     "the --rmult, --gmult, and --bmult options");
        if (!(rmultSpec && gmultSpec && bmultSpec))
            pm_error("If you specify any of --rmult, --gmult, or --bmult, "
                     "you must specify all of them");
        maxLuminance =
            cmdlineP->color2gray.rfrac +
            cmdlineP->color2gray.gfrac +
            cmdlineP->color2gray.bfrac;
        if (fabsf(1.0f - maxLuminance) > REAL_EPSILON)
            pm_error("The values given for --rmult, --gmult, and --bmult must "
                     "sum to 1.0, not %.10g", maxLuminance);
    } else if (csSpec)
        cmdlineP->color2gray = color2GrayFromCsName(colorspaceOpt);
    else
        cmdlineP->color2gray = color2GrayFromCsName("ntsc");

    if (colorfileSpec && cmdlineP->targetcolorSpec)
        pm_error("The --colorfile option and the --targetcolor option are "
                 "mutually exclusive");

    if (!colorfileSpec)
        cmdlineP->colorfile = NULL;

    if (cmdlineP->targetcolorSpec) {
        sample const colorMaxVal = (1<<16) - 1;
            /* Maximum PAM maxval for precise sample-to-float conversion */
        tuple const targetTuple = pnm_parsecolor(targetcolorOpt, colorMaxVal);
        cmdlineP->targetcolor.rfrac =
            targetTuple[PAM_RED_PLANE] / (float)colorMaxVal;
        cmdlineP->targetcolor.gfrac =
            targetTuple[PAM_GRN_PLANE] / (float)colorMaxVal;
        cmdlineP->targetcolor.bfrac =
            targetTuple[PAM_BLU_PLANE] / (float)colorMaxVal;
    }

    if (argc-1 < 1)
        cmdlineP->inputFileName = "-";
    else {
        cmdlineP->inputFileName = argv[1];
        if (argc-1 > 1)
            pm_error("Too many arguments: %u.  The only argument is the "
                     "optional input file name", argc-1);
    }
}



int
main(int argc, const char *argv[]) {
    struct cmdlineInfo cmdline;          /* Command-line parameters */
    struct pam         inPam;
    struct pam         outPam;
    struct pam         colorPam;
    FILE *             ifP;
    FILE *             colorfP;
    const char *       comments;
    tuplen *           inRow;
    tuplen *           outRow;
    tuplen **          colorData;
    tuplen *           colorRowBuffer;
    unsigned int       row;

    pm_proginit(&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    srand(cmdline.randomseedSpec ? cmdline.randomseed : pm_randseed());

    ifP = pm_openr(cmdline.inputFileName);
    inPam.comment_p = &comments;
    pnm_readpaminit(ifP, &inPam, PAM_STRUCT_SIZE(comment_p));

    outPam = inPam;
    outPam.file = stdout;
    outPam.format = PAM_FORMAT;
    outPam.depth = 4 - (inPam.depth % 2);
    outPam.allocation_depth = outPam.depth;
    strcpy(outPam.tuple_type, PAM_PPM_TUPLETYPE);
    pnm_writepaminit(&outPam);

    if (cmdline.colorfile) {
        colorfP = pm_openr(cmdline.colorfile);
        colorPam.comment_p = NULL;
        colorData =
            pnm_readpamn(colorfP, &colorPam, PAM_STRUCT_SIZE(comment_p));
    } else {
        colorfP = NULL;
        colorPam = outPam;
        colorData = NULL;
    }

    inRow = pnm_allocpamrown(&inPam);
    outRow = pnm_allocpamrown(&outPam);

    colorRowBuffer = pnm_allocpamrown(&outPam);

    for (row = 0; row < inPam.height; ++row) {
        tuplen * colorRow;

        pnm_readpamrown(&inPam, inRow);

        if (cmdline.colorfile)
            colorRow = getColorRow(&colorPam, colorData, row, outPam.width);
        else {
            colorRow = colorRowBuffer;

            if (cmdline.targetcolorSpec)
                explicitlyColorRow(&colorPam, colorRow, cmdline.targetcolor);
            else
                randomlyColorRow(&colorPam, colorRow);
        }
        recolorRow(&inPam, inRow,
                   &cmdline.color2gray, colorRow,
                   &outPam, outRow);
        pnm_writepamrown(&outPam, outRow);
    }
    pnm_freepamrown(outRow);
    pnm_freepamrown(inRow);
    pnm_freepamrown(colorRowBuffer);

    if (colorData)
        pnm_freepamarrayn(colorData, &colorPam);

    if (colorfP)
        pm_close(colorfP);
    pm_close(ifP);

    return 0;
}

