/* eventtxt.c: event text log to human-friendly format.
 * 
 * $Id: //info.ravenbrook.com/project/mps/version/1.113/code/eventtxt.c#1 $
 * 
 * Copyright (c) 2012-2013 Ravenbrook Limited.  See end of file for license.
 *
 * This is a command-line tool that converts events from a text-format
 * MPS telemetry file into a more human-readable format.
 *
 * The default MPS library will write a binary-format telemetry file
 * which can be converted into a text-format file using the eventcnv
 * program (q.v.).
 *
 * For efficiency, eventcnv writes all event times, codes, and
 * parameters (apart from EventFS - strings - and EventFD -
 * floating-point) as hexadecimal strings, separated by single spaces.
 * For human-readable purposes, we'd prefer a format in which
 * parameters are named; event codes are converted to event type
 * names; integers are in decimal; booleans are 'True ' or 'False';
 * pointers, addresses, and words are in hex; and labelled addresses
 * are shown with their label strings.  This program performs that
 * conversion.
 *
 * Options:
 * 
 * -l <logfile>: Import events from the named logfile.  Defaults to
 * stdin.
 *
 * $Id: //info.ravenbrook.com/project/mps/version/1.113/code/eventtxt.c#1 $
 */

#include "config.h"
#include "eventdef.h"
#include "eventcom.h"
#include "table.h"
#include "testlib.h" /* for ulongest_t and associated print formats */

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

#ifdef MPS_BUILD_MV
/* MSVC warning 4996 = stdio / C runtime 'unsafe' */
/* Objects to: strncpy, sscanf, fopen.  See job001934. */
#pragma warning( disable : 4996 )
#endif

static const char *prog; /* program name */
static const char *logFileName = NULL;

/* everror -- error signalling */

static void everror(const char *format, ...)
{
  va_list args;

  fflush(stdout); /* sync */
  fprintf(stderr, "%s: ", prog);
  va_start(args, format);
  vfprintf(stderr, format, args);
  fprintf(stderr, "\n");
  va_end(args);
  exit(EXIT_FAILURE);
}

static void usage(void)
{
  fprintf(stderr,
          "Usage: %s [-l <logfile>]\n",
          prog);
}

static void usageError(void)
{
        usage();
        everror("Bad usage");
}

/* parseArgs -- parse command line arguments */

static void parseArgs(int argc, char *argv[])
{
  int i = 1;

  if (argc >= 1)
    prog = argv[0];
  else
    prog = "unknown";

  while (i < argc) { /* consider argument i */
    if (argv[i][0] == '-') { /* it's an option argument */
      switch (argv[i][1]) {
      case 'l': /* log file name */
        ++ i;
        if (i == argc)
          usageError();
        else
          logFileName = argv[i];
        break;
      case '?': case 'h': /* help */
        usage();
        exit(EXIT_SUCCESS);
      default:
        usageError();
      }
    } /* if option */
    ++ i;
  }
}

/* table methods for a table of interned strings, and another of
 * labelled addresses. */

static void *tableAlloc(void *closure, size_t size)
{
  UNUSED(closure);
  return malloc(size);
}

static void tableFree(void *closure, void *p, size_t size)
{
  UNUSED(closure);
  UNUSED(size);
  free(p);
}

/* Printing routines */

/* printStr -- print an EventString */

static void printStr(const char *str)
{
  size_t i;

  putchar('"');
  for (i = 0; str[i] != '\0'; ++i) {
    char c = str[i];
    if (c == '"' || c == '\\') putchar('\\');
    putchar(c);
  }
  putchar('"');
}


/* Reading hex numbers, and doubles, and quoted-and-escaped
 * strings. */

static ulongest_t parseHex(char **pInOut)
{
  ulongest_t val;
  int i, l;
  char *p = *pInOut;

  i = sscanf(p, "%" SCNXLONGEST "%n", &val, &l);
  if (i != 1)
    everror("Couldn't read a hex number from '%s'", p);
  *pInOut = p + l;
  return val;
}

static double parseDouble(char **pInOut)
{
  double val;
  int i, l;
  char *p = *pInOut;

  i = sscanf(p, "%lg%n", &val, &l);
  if (i != 1)
    everror("Couldn't read a float from '%s'", p);
  *pInOut = p + l;
  return val;
}

/* parseString checks string syntax (opening and closing quotation
 * marks) and takes a copy (stripping escaping backslashes) into a
 * static buffer (callers must "use it or lose it"; the next
 * invocation will over-write it).  Probably not bullet-proof. */

#define MAX_STRING_LENGTH 1024

char strBuf[MAX_STRING_LENGTH];

static char *parseString(char **pInOut)
{
  char *p = *pInOut;
  char *q = strBuf;
  while(*p == ' ')
    ++p;
        
  if (*p != '"')
    everror("String has no opening quotation mark: '%s'", p);
  ++p;

  while(1) {
    if (q - strBuf >= MAX_STRING_LENGTH) {
      everror("String length exceeds %d", MAX_STRING_LENGTH);
    }
    if (*p == '\\') { /* escaped character */
      ++p;
      if (*p == '\0')
        everror("Closing NUL byte escaped by backslash.");
      *q++ = *p++;
    } else if (*p == '"') { /* end of string */
      *q = '\0';
      ++p;
      *pInOut = p;
      return strBuf;
    } else if (*p == '\0')
      everror("Unexpected closing NUL byte.");
    else
      *q++ = *p++;
  }
}

/* Event logs have interned strings (i.e. they construct a partial
 * function from non-negatie integer IDs to strings), and can label
 * addresses with intern string IDs (i.e. they construct a partial
 * function from address to string ID). We need two tables to keep
 * track of these. */

static Table internTable;      /* dictionary of intern ids to strings */

static Table labelTable;       /* dictionary of addrs to intern ids */

static void createTables(void)
{
  Res res;
  /* MPS intern IDs are serials from zero up, so we can use -1
   * and -2 as specials. */
  res = TableCreate(&internTable,
                    (size_t)1<<4,
                    tableAlloc, tableFree, NULL,
                    (Word)-1, (Word)-2); 
  if (res != ResOK)
    everror("Couldn't make intern table.");

  /* We assume that 0 and 1 are invalid as Addrs. */
  res = TableCreate(&labelTable, (size_t)1<<7,
                    tableAlloc, tableFree, NULL,
                    0, 1);
  if (res != ResOK)
    everror("Couldn't make label table.");
}

/* recordIntern -- record an interned string in the table.  a copy of
* the string from the parsed buffer into a newly-allocated block. */

static void recordIntern(char *p)
{
  ulongest_t stringId;
  char *string;
  char *copy;
  size_t len;
  Res res;
        
  stringId = parseHex(&p);
  string = parseString(&p);
  len = strlen(string);
  copy = malloc(len+1);
  if (copy == NULL)
    everror("Couldn't allocate space for a string.");
  (void)strcpy(copy, string);
  res = TableDefine(internTable, (Word)stringId, (void *)copy);
  if (res != ResOK)
    everror("Couldn't create an intern mapping.");
}

/* recordLabel records a label (an association between an address and
 * a string ID).  Note that the event log may have been generated on a
 * platform with addresses larger than Word on the current platform.
 * If that happens then we are scuppered because our Table code uses
 * Word as the key type: there's nothing we can do except detect this
 * bad case (see also the EventInit handling and warning code).
 *
 * We can and do handle the case where string IDs (which are Words on
 * the MPS platform) are larger than void* on the current platform.
 * This is probably in fact the same case, because Word should be the
 * same size as void*.  In practice, trying to analyse a log from a
 * wide platform on a narrow one (e.g. - the only case which is likely
 * to occur this decade - from a 64-bit platform on a 32-bit one) is
 * probably a bad idea and maybe doomed to failure.
 */

static void recordLabel(char *p)
{
  ulongest_t address;
  ulongest_t *stringIdP;
  Res res;
        
  address = parseHex(&p);
  if (address > (Word)-1) {
    printf("label address too large!");
    return;
  }
                
  stringIdP = malloc(sizeof(ulongest_t));
  if (stringIdP == NULL)
    everror("Can't allocate space for a string's ID");
  *stringIdP = parseHex(&p);
  res = TableDefine(labelTable, (Word)address, (void *)stringIdP);
  if (res != ResOK)
    everror("Couldn't create an intern mapping.");
}

/* output code */

/* hexWordWidth is the number of characters used to output a Word
 * value in hexadecimal.  Note that what we really care about is the
 * width of a Word on the source platform, not here.  So when we see
 * an EventInit event, we update this variable to the necessary
 * width. */

static int hexWordWidth = (MPS_WORD_WIDTH+3)/4;

/* printAddr -- output a ulongest_t in hex, with the interned string
 * if the value is in the label table */

static void printAddr(ulongest_t addr, const char *ident)
{
  ulongest_t label;
  void *alias;
        
  printf("%s:%0*" PRIXLONGEST, ident, hexWordWidth, addr);
  if (TableLookup(&alias, labelTable, addr)) {
    label = *(ulongest_t*)alias;
    putchar('[');
    if (TableLookup(&alias, internTable, label))
      printStr((char *)alias);
    else
      printf("unknown label %" PRIuLONGEST, label);
    putchar(']');
  }
  putchar(' ');
}

/* parameter processing.  For each parameter we parse it and then
 * print it, preceded by its name and a colon and followed by a
 * space. */

#define processParamA(ident)          \
        val_hex = parseHex(&p);       \
        printAddr(val_hex, #ident);

#define processParamP processParamA
#define processParamW processParamA

#define processParamU(ident)          \
        val_hex = parseHex(&p);       \
        printf(#ident ":%" PRIuLONGEST " ", val_hex);

#define processParamD(ident)          \
        val_float = parseDouble(&p);  \
        printf(#ident ":%#8.3g ", val_float);

#define processParamS(ident)          \
        val_string = parseString(&p); \
        printf(#ident ":");           \
        printStr(val_string);         \
        putchar(' ');

#define processParamB(ident)          \
        val_hex = parseHex(&p);       \
        printf(#ident ":%s ", val_hex ? "True" : "False");
        
#define EVENT_PROCESS_PARAM(X, index, sort, ident) processParam##sort(ident);

#define EVENT_PROCESS(X, name, code, always, kind) \
        case code: \
                EVENT_##name##_PARAMS(EVENT_PROCESS_PARAM, X) \
        break;

/* a table of the event names */

static const char *eventName[EventCodeMAX+EventCodeMAX];

#define EVENT_SET_NAME(X, name, code, always, kind) \
        eventName[code] = #name;

/* this is overkill, at present. */

#define MAX_LOG_LINE_LENGTH 1024

/* readLog -- read and parse log.  Returns the number of events written.  */

static void readLog(FILE *input)
{
  int i;

  for (i=0; i <= EventCodeMAX; ++i)
    eventName[i] = NULL;

  EVENT_LIST(EVENT_SET_NAME, X);

  while (TRUE) { /* loop for each event */
    char line[MAX_LOG_LINE_LENGTH];
    char *p, *q;
    ulongest_t clock;
    int code;
    ulongest_t val_hex;
    double val_float;
    const char *val_string;

    p = fgets(line, MAX_LOG_LINE_LENGTH, input);
    if (!p) {
      if (feof(input))
        break;
      else
        everror("Couldn't read line from input.");
    }

    clock = parseHex(&p);
    code = (int)parseHex(&p);

    if (eventName[code])
      printf("%0*" PRIXLONGEST " %04X %-19s ", hexWordWidth, clock, code,
             eventName[code]);
    else 
      printf("%0*" PRIXLONGEST " %04X %-19s ", hexWordWidth, clock, code,
             "[Unknown]");

    q = p;

    /* for a few particular codes, we do local processing. */
    if (code == EventInternCode) {
      recordIntern(q);
    } else if (code == EventLabelCode) {
      recordLabel(q);
    } else if (code == EventEventInitCode) {
      ulongest_t major, median, minor, maxCode, maxNameLen, wordWidth, clocksPerSec;
      major = parseHex(&q);  /* EVENT_VERSION_MAJOR */
      median = parseHex(&q); /* EVENT_VERSION_MEDIAN */
      minor = parseHex(&q);  /* EVENT_VERSION_MINOR */
      maxCode = parseHex(&q); /* EventCodeMAX */
      maxNameLen = parseHex(&q); /* EventNameMAX */
      wordWidth = parseHex(&q); /* MPS_WORD_WIDTH */
      clocksPerSec = parseHex(&q); /* mps_clocks_per_sec() */
      UNUSED(clocksPerSec);
      UNUSED(maxNameLen);

      if ((major != EVENT_VERSION_MAJOR) ||
          (median != EVENT_VERSION_MEDIAN) ||
          (minor != EVENT_VERSION_MINOR)) {
        fprintf(stderr, "Event log version does not match: %d.%d.%d vs %d.%d.%d\n",
                (int)major, (int)median, (int)minor,
                EVENT_VERSION_MAJOR,
                EVENT_VERSION_MEDIAN,
                EVENT_VERSION_MINOR);
      }

      if (maxCode > EventCodeMAX) {
        fprintf(stderr, "Event log may contain unknown events with codes from %d to %d\n",
                EventCodeMAX+1, (int)maxCode);
      }

      if (wordWidth > MPS_WORD_WIDTH) {
        int newHexWordWidth = (int)((wordWidth + 3) / 4);
        if (newHexWordWidth > hexWordWidth) {
          fprintf(stderr,
                  "Event log word width is greater than on current platform;"
                  "previous values may be printed too narrowly.\n");
        }
        hexWordWidth = newHexWordWidth;
      }
      
      if (wordWidth > sizeof(ulongest_t) * CHAR_BIT) {
        everror("Event log word width %d is too wide for the current platform.",
                (int)wordWidth);
      }
    }

    switch(code) {
      EVENT_LIST(EVENT_PROCESS, X);
    default:
      printf("Unknown event.");
    }
    putchar('\n');

  }
}

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

  parseArgs(argc, argv);
  if (!logFileName) {
    input = stdin;
    logFileName = "<stdin>";
  } else {
    input = fopen(logFileName, "r");
    if (input == NULL)
      everror("unable to open %s", logFileName);
  }

  createTables();
  readLog(input);
  (void)fclose(input);
  return 0;
}

/* C. COPYRIGHT AND LICENSE
 *
 * Copyright (C) 2012-2013 Ravenbrook Limited <http://www.ravenbrook.com/>.
 * All rights reserved.  This is an open source license.  Contact
 * Ravenbrook for commercial licensing options.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 
 * 3. Redistributions in any form must be accompanied by information on how
 * to obtain complete source code for this software and any accompanying
 * software that uses this software.  The source code must either be
 * included in the distribution or be available for no more than the cost
 * of distribution plus a nominal fee, and must be freely redistributable
 * under reasonable conditions.  For an executable file, complete source
 * code means the source code for all modules it contains. It does not
 * include source code for modules or files that typically accompany the
 * major components of the operating system on which the executable file
 * runs.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, OR NON-INFRINGEMENT, ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
