/* pbmtonokia.c - convert a PBM image to Nokia Smart Messaging
   Formats (NOL, NGG, HEX)

   Copyright information is at end of file.
*/

#define _DEFAULT_SOURCE /* New name for SVID & BSD source defines */
#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
#define _BSD_SOURCE    /* Make sure strcaseeq() is in nstring.h */
#include <string.h>
#include <assert.h>

#include "pm_c_util.h"
#include "nstring.h"
#include "mallocvar.h"
#include "shhopt.h"
#include "pbm.h"

enum outputFormat {
    FMT_HEX_NOL,
    FMT_HEX_NGG,
    FMT_HEX_NPM,
    FMT_NOL,
    FMT_NGG,
    FMT_NPM
};


struct cmdlineInfo {
    /* All the information the user supplied in the command line,
       in a form easy for the program to use.
    */
    const char * inputFileName;  /* Filename of input files */
    int outputFormat;
    const char * networkCode;
    const char * txt;  /* NULL means unspecified */
};



static const char *
uppercase(const char * const subject) {

    char * buffer;

    buffer = malloc(strlen(subject) + 1);

    if (buffer == NULL)
        pm_error("Out of memory allocating buffer for uppercasing a "
                 "%u-character string", (unsigned)strlen(subject));
    else {
        unsigned int i;

        i = 0;
        while (subject[i]) {
            buffer[i] = TOUPPER(subject[i]);
            ++i;
        }
        buffer[i] = '\0';
    }
    return buffer;
}



static void
parseCommandLine(int argc, char ** 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;
        /* Instructions to pm_optParseOptions3 on how to parse our options.
         */
    optStruct3 opt;

    unsigned int option_def_index;
    unsigned int fmtSpec, netSpec, txtSpec;
    const char * fmtOpt;
    const char * netOpt;

    MALLOCARRAY_NOFAIL(option_def, 100);

    option_def_index = 0;   /* incremented by OPTENT3 */
    OPTENT3(0, "fmt",     OPT_STRING, &fmtOpt,
            &fmtSpec, 0);
    OPTENT3(0, "net",     OPT_STRING, &netOpt,
            &netSpec, 0);
    OPTENT3(0, "txt",     OPT_STRING, &cmdlineP->txt,
            &txtSpec, 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 */

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

    if (fmtSpec) {
        if (strcaseeq(fmtOpt, "HEX_NOL"))
            cmdlineP->outputFormat = FMT_HEX_NOL;
        else if (strcaseeq(fmtOpt, "HEX_NGG"))
            cmdlineP->outputFormat = FMT_HEX_NGG;
        else if (strcaseeq(fmtOpt, "HEX_NPM"))
            cmdlineP->outputFormat = FMT_HEX_NPM;
        else if (strcaseeq(fmtOpt, "NOL"))
            cmdlineP->outputFormat = FMT_NOL;
        else if (strcaseeq(fmtOpt, "NGG"))
            cmdlineP->outputFormat = FMT_NGG;
        else if (strcaseeq(fmtOpt, "NPM"))
            cmdlineP->outputFormat = FMT_NPM;
        else
            pm_error("-fmt option must be HEX_NGG, HEX_NOL, HEX_NPM, "
                     "NGG, NOL or NPM.  You specified '%s'", fmtOpt);
    } else
        cmdlineP->outputFormat = FMT_HEX_NOL;

    if (netSpec) {
        if (strlen(netOpt) != 6)
            pm_error("-net option must be 6 hex digits long.  "
                     "You specified %u characters", (unsigned)strlen(netOpt));
        else if (!pm_strishex(netOpt))
            pm_error("-net option must be hexadecimal.  You specified '%s'",
                     netOpt);
        else
            cmdlineP->networkCode = uppercase(netOpt);
    } else
        cmdlineP->networkCode = strdup("62F210");  /* German D1 net */

    if (!txtSpec)
        cmdlineP->txt = NULL;
    else if (strlen(cmdlineP->txt) > 120)
        pm_error("Text message is longer (%u characters) than "
                 "the 120 characters allowed by the format.",
                 (unsigned)strlen(cmdlineP->txt));

    if (argc-1 == 0)
        cmdlineP->inputFileName = "-";
    else if (argc-1 != 1)
        pm_error("Program takes zero or one argument (filename).  You "
                 "specified %u", argc-1);
    else
        cmdlineP->inputFileName = argv[1];
}



static void
freeCmdline(struct cmdlineInfo const cmdline) {

    pm_strfree(cmdline.networkCode);
}



static void
validateSize(unsigned int const cols,
             unsigned int const rows){

    if (cols > 255)
        pm_error("This program cannot handle files with more than 255 "
                 "columns");
    if (rows > 255)
        pm_error("This program cannot handle files with more than 255 "
                 "rows");
}




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

    unsigned int row;

    /* header */
    fprintf(ofP, "06050415820000%s00%02X%02X01", networkCode, cols, rows);

    /* image */
    for (row = 0; row < rows; ++row) {
        unsigned int col;
        unsigned int p;
        unsigned int c;

        c = 0;

        for (p = 0, col = 0; col < cols; ++col) {
            if (image[row][col] == PBM_BLACK)
                c |= 0x80 >> p;
            if (++p == 8) {
                fprintf(ofP, "%02X",c);
                p = c = 0;
            }
        }
        if (p > 0)
            fprintf(ofP, "%02X", c);
    }
}



static void
convertToHexNgg(bit **       const image,
                unsigned int const cols,
                unsigned int const rows,
                FILE *       const ofP) {

    unsigned int row;

    /* header */
    fprintf(ofP, "0605041583000000%02X%02X01", cols, rows);

    /* image */
    for (row = 0; row < rows; ++row) {
        unsigned int col;
        unsigned int p;
        unsigned int c;

        for (p = 0, c = 0, col = 0; col < cols; ++col) {
            if (image[row][col] == PBM_BLACK)
                c |= 0x80 >> p;
            if (++p == 8) {
                fprintf(ofP, "%02X", c);
                p = c = 0;
            }
        }
        if (p > 0)
            fprintf(ofP, "%02X", c);
    }
}




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

    unsigned int row;

    /* header */
    fprintf(ofP, "060504158A0000");

    /* text */
    if (text) {
        size_t const len = strlen(text);

        unsigned int it;

        fprintf(ofP, "00%04X", (unsigned)len);

        for (it = 0; it < len; ++it)
            fprintf(ofP, "%02X", text[it]);
    }

    /* image */
    fprintf(ofP, "02%04X00%02X%02X01", (cols * rows) / 8 + 4, cols, rows);

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

        for (p = 0, c = 0, col = 0; col < cols; ++col) {
            if (image[row][col] == PBM_BLACK)
                c |= 0x80 >> p;
            if (++p == 8) {
                fprintf(ofP, "%02X", c);
                p = c = 0;
            }
        }
        if (p > 0)
            fprintf(ofP, "%02X", c);
    }
}



static void
convertToNol(bit **       const image,
             unsigned int const cols,
             unsigned int const rows,
             FILE *       const ofP) {

    unsigned int row;
    char header[32];
    unsigned int it;

    /* header - this is a hack */

    header[ 0] = 'N';
    header[ 1] = 'O';
    header[ 2] = 'L';
    header[ 3] = 0;
    header[ 4] = 1;
    header[ 5] = 0;
    header[ 6] = 4;
    header[ 7] = 1;
    header[ 8] = 1;
    header[ 9] = 0;
    header[10] = cols;
    header[11] = 0;
    header[12] = rows;
    header[13] = 0;
    header[14] = 1;
    header[15] = 0;
    header[16] = 1;
    header[17] = 0;
    header[18] = 0x53;
    header[19] = 0;

    fwrite(header, 20, 1, ofP);

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

        for (col = 0; col < cols; ++col) {
            char const output = image[row][col] == PBM_BLACK ? '1' : '0';

            putc(output, ofP);
        }
    }

    /* padding (to keep gnokii happy) */
    for (it = 0; it < 8 - cols * rows % 8; ++it)
        putc('0', ofP);
}




static void
convertToNgg(bit **       const image,
             unsigned int const cols,
             unsigned int const rows,
             FILE *       const ofP) {

    unsigned int row;
    char    header[32];
    unsigned int it;

    /* header - this is a hack */

    header[ 0] = 'N';
    header[ 1] = 'G';
    header[ 2] = 'G';
    header[ 3] = 0;
    header[ 4] = 1;
    header[ 5] = 0;
    header[ 6] = cols;
    header[ 7] = 0;
    header[ 8] = rows;
    header[ 9] = 0;
    header[10] = 1;
    header[11] = 0;
    header[12] = 1;
    header[13] = 0;
    header[14] = 0x4a;
    header[15] = 0;

    fwrite(header, 16, 1, ofP);

    /* image */

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

        for (col = 0; col < cols; ++col) {
            char const output = image[row][col] == PBM_BLACK ? '1' : '0';

            putc(output, ofP);
        }
    }

    /* padding (to keep gnokii happy) */
    for (it = 0; it < 8 - cols * rows % 8; ++it)
        putc('0', ofP);
}



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

    size_t const textLen = text ? strlen(text) : 0;

    unsigned int row;
    char header[132];

    /* header and optional text */

    /* Our entry condition is that 'text' be a legal text field, which means
       no more than 120 characters:
    */
    assert(textLen <= 120);

    /* We don't have any specification of this format, but in May 2020, we
       found what looks like a bug: the memcpy of the text field started at
       &header[5] (overwriting the previous setting of header[5] and leaving
       header[6+len-1] not set to anything).  Nobody ever complained about
       this, so maybe somehow it worked better than the sensible code we have
       now, where the text field starts in the 7th byte.
    */

    /* The code below that deliberately avoids using 'text' as an argument to
       memcpy when 'text' is null looks like it should have no practical
       effect, because with a zero length memcpy, it shouldn't matter what the
       from and to arguments are.  But it's logically correct - a null pointer
       just doesn't point anywhere.  And believe it or not, it has a practical
       effect - a truly bizarre one.  This code used to be simpler and just
       pass that null pointer to memcpy, with length 0, and as reported in May
       2020, a new optimizing compiler caused header[3] to contain random
       values.  That's right: 3.  Skipping that presumed no-op memcpy stopped
       the behavior.
    */

    header[           0] = 'N';
    header[           1] = 'P';
    header[           2] = 'M';
    header[           3] = 0;
    header[           4] = textLen;
    header[           5] = 0;
    if (text)  /* see above */
        memcpy(&header[6], text, textLen);
    header[ 6 + textLen] = cols;
    header[ 7 + textLen] = rows;
    header[ 8 + textLen] = 1;
    header[ 9 + textLen] = 1;
    header[10 + textLen] = 0; /* unknown */

    assert(10 + textLen < sizeof(header));

    fwrite(header, 11 + textLen, 1, ofP);

    /* image: stream of bits, each row padded to a byte boundary
       inspired by gnokii/common/gsm-filesystems.c
     */
    for (row = 0; row < rows; row++) {
        unsigned int byteNumber;
        int bitNumber;
        char buffer[32];  /* picture messages are (always?) 72 x 28 */
        unsigned int col;

        byteNumber = 0;
        bitNumber = 7;

        memset(buffer, 0, sizeof(buffer));

        for (col = 0; col < cols; ++col) {
            if (image[row][col] == PBM_BLACK)
                buffer[byteNumber] |= (1 << bitNumber);
            --bitNumber;
            if (bitNumber < 0 && col < (cols - 1)) {
                bitNumber = 7;
                ++byteNumber;
            }
        }
        fwrite(buffer, byteNumber + 1, 1, ofP);
    }
}



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

    struct cmdlineInfo cmdline;
    FILE  * ifP;
    bit ** bits;
    int rows, cols;

    pbm_init(&argc, argv);

    parseCommandLine(argc, argv, &cmdline);

    ifP = pm_openr(cmdline.inputFileName);
    bits = pbm_readpbm(ifP, &cols, &rows);
    pm_close(ifP);

    validateSize(cols, rows);

    switch (cmdline.outputFormat) {
    case FMT_HEX_NGG:
        convertToHexNgg(bits, cols, rows, stdout);
        break;
    case FMT_HEX_NOL:
        convertToHexNol(bits, cols, rows, cmdline.networkCode, stdout);
        break;
    case FMT_HEX_NPM:
        convertToHexNpm(bits, cols, rows, cmdline.txt, stdout);
        break;
    case FMT_NGG:
        convertToNgg(bits, cols, rows, stdout);
        break;
    case FMT_NOL:
        convertToNol(bits, cols, rows, stdout);
        break;
    case FMT_NPM:
        convertToNpm(bits, cols, rows, cmdline.txt, stdout);
        break;
    }

freeCmdline(cmdline);

    return 0;
}



/* Copyright (C)2001 OMS Open Media System GmbH, Tim Rühsen
** <tim.ruehsen@openmediasystem.de>.
**
** 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.

  Created 2001.06.07

Notes:
  - limited to rows <= 255 and columns <= 255
  - limited to b/w graphics, not animated

Testing:
  Testing was done with SwissCom SMSC (Switzerland) and IC3S SMSC (Germany).
  The data was send with EMI/UCP protocol over TCP/IP.

  - 7.6.2001: tested with Nokia 3210: 72x14 Operator Logo
  - 7.6.2001: tested with Nokia 6210: 72x14 Operator Logo and
              72x14 Group Graphic
*/
