/* pcb copyright notice... */

/* This file defines a wrapper around sprintf, that
 *  defines new specifiers that take pcb BDimension
 *  objects as input.
 *
 * There is a fair bit of nasty (repetitious) code in
 *  here, but I feel the gain in clarity for output
 *  code elsewhere in the project will make it worth
 *  it.
 *
 * The new specifiers are:
 *   %mm    output a measure in mm
 *   %mM    output a measure in scaled (mm/um) metric
 *   %ml    output a measure in mil
 *   %mL    output a measure in scaled (mil/in) imperial
 *   %ms    output a measure in most natural mm/mil units
 *   %mS    output a measure in most natural scaled units
 *   %md    output a pair of measures in most natural mm/mil units
 *   %mD    output a pair of measures in most natural scaled units
 *   %m3    output 3 measures in most natural scaled units
 *     ...
 *   %m9    output 9 measures in most natural scaled units
 *   %m*    output a measure with unit given as an additional
 *          const char* parameter
 *   %mr    output a measure in a unit readable by parse_l.l
 *
 * These accept the usual printf modifiers for %f,
 *  as well as the additional modifier $ which is
 *  used to output a unit suffix after the measure.
 *
 * KNOWN ISSUES:
 *   No support for %zu size_t printf spec
 *   No support for .* subspecifier for pcb specs
 */

#include <math.h>
#include <ctype.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>

/* Ripped from PCB to make test driver standalone */
typedef int bool;
typedef int BDimension;

#define COORD_TO_MIL(n)	((n) / 100.0)
#define MIL_TO_COORD(n)	((n) * 100.0)
#define COORD_TO_MM(n)	((n) * 0.000254)
#define MM_TO_COORD(n)	((n) / 0.000254)
/* end rip */

enum e_allow {
  ALLOW_NM = 1,
  ALLOW_UM = 2,
  ALLOW_MM = 4,
  ALLOW_CM = 8,
  ALLOW_M  = 16,
  ALLOW_KM = 32,

  ALLOW_CMIL = 1024,
  ALLOW_MIL  = 2048,
  ALLOW_IN   = 4096,

  ALLOW_METRIC   = ALLOW_NM | ALLOW_UM | ALLOW_MM |
                   ALLOW_CM | ALLOW_M  | ALLOW_KM,
  ALLOW_IMPERIAL = ALLOW_CMIL | ALLOW_MIL | ALLOW_IN,
  ALLOW_READABLE = ALLOW_UM | ALLOW_MM | ALLOW_MIL | ALLOW_IN,

  ALLOW_ALL = ~0
};

enum e_family { METRIC, IMPERIAL };

struct unit {
  const char *suffix;
  char printf_code;
  double scale_factor;
  enum e_family family;
  enum e_allow  allow;
  int default_prec;
};

/* These should be kept in order of smallest scale_factor
 * to largest -- the code uses this ordering when finding
 * the best scale to use for a group of measures */
static struct unit Units[] = {
  { "km", 'k', 0.000001, METRIC, ALLOW_KM, 5 },
  { "m",  'f', 0.001,    METRIC, ALLOW_M,  5 },
  { "cm", 'e', 0.1,      METRIC, ALLOW_CM, 5 },
  { "mm", 'm', 1,        METRIC, ALLOW_MM, 4 },
  { "um", 'u', 1000,     METRIC, ALLOW_UM, 2 },
  { "nm", 'n', 1000000,  METRIC, ALLOW_NM, 0 },

  { "in",   'i', 0.001, IMPERIAL, ALLOW_IN,   5 },
  { "mil",  'l', 1,     IMPERIAL, ALLOW_MIL,  2 },
  { "cmil", 'c', 100,   IMPERIAL, ALLOW_CMIL, 0 }
};
#define N_UNITS ((int) (sizeof Units / sizeof Units[0]))

static int min_sig_figs(double d)
{
  char buf[50];
  int rv;

  if(d == 0) return 0;

  /* Normalize to x.xxxx... form */
  if(d < 0)      d *= -1;
  while(d >= 10) d /= 10;
  while(d < 1)   d *= 10;

  sprintf(buf, "%g%n", d, &rv);
  return rv;
}


/* Converts a (group of) measurement(s) to a comma-deliminated
 * string, with appropriate units. If more than one coord is
 * given, the list is enclosed in parens to make the scope of
 * the unit suffix clear.  */
static char *CoordsToString(BDimension coord[], int n_coords, const char *printf_spec, enum e_allow allow, bool add_suffix)
{
  char *buff, *pbuff;
  char printf_buff[100];
  enum e_family family;
  double *value;
  const char *suffix;
  int i, n;

  value = malloc (n_coords * sizeof *value);
  buff  = malloc (n_coords * 50);  /* TODO: max 50 chars per unit */

  /* Sanity checks */
  if (buff == NULL || value == NULL || strlen(printf_spec) > (sizeof printf_buff) - 10)
    return NULL;
  if (allow == 0)
    allow = ALLOW_ALL;

  /* Check our freedom in choosing units */
  if ((allow & ALLOW_IMPERIAL) == 0)
    family = METRIC;
  else if ((allow & ALLOW_METRIC) == 0)
    family = IMPERIAL;
  else
    {
      int met_votes = 0,
          imp_votes = 0;

      for (i = 0; i < n_coords; ++i)
        if(min_sig_figs(COORD_TO_MIL(coord[i])) < min_sig_figs(COORD_TO_MM(coord[i])))
          ++imp_votes;
        else
          ++met_votes;

      if (imp_votes > met_votes)
        family = IMPERIAL;
      else
        family = METRIC;
    }

  /* Set base unit */
  for (i = 0; i < n_coords; ++i)
    {
      switch (family)
        {
        case METRIC:   value[i] = COORD_TO_MM (coord[i]); break;
        case IMPERIAL: value[i] = COORD_TO_MIL (coord[i]); break;
        }
    }

  /* Determine scale factor -- find smallest unit that brings
   * the whole group above unity */
  for (n = 0; n < N_UNITS; ++n)
    {
      if ((Units[n].allow & allow) != 0 && (Units[n].family == family))
        {
          int n_above_one = 0;
    
          for (i = 0; i < n_coords; ++i)
            if (value[i] * Units[n].scale_factor > 1 || coord[0] == 0)
              ++n_above_one;
          if (n_above_one == n_coords)
            break;
        }
    }
  /* If nothing worked, just fall to the smallest allowable unit */
  if (n == N_UNITS)
    {
      n = 0;
      while ((Units[n].allow & allow) == 0 || Units[n].family != family)
        ++n;
    }

  /* Apply scale factor */
  suffix = Units[n].suffix;
  for (i = 0; i < n_coords; ++i)
    value[i] = value[i] * Units[n].scale_factor;

  /* Create sprintf specifier */
  if (printf_spec && *printf_spec)
    sprintf(printf_buff, ", %%%sf%%n", printf_spec);
  else
    sprintf(printf_buff, ", %%.%df%%n", Units[n].default_prec);

  /* Actually sprintf the values in place
   *  (+ 2 skips the ", " for first value) */
  pbuff = buff;
  if (n_coords > 1)
    *pbuff++ = '(';
  sprintf(pbuff, printf_buff + 2, value[0], &n);
  for (i = 1; i < n_coords; ++i)
    {
      pbuff += n;
      sprintf(pbuff, printf_buff, value[i], &n);
    }
  if (n_coords > 1)
    pbuff[n++] = ')';
  if (add_suffix)
    sprintf(pbuff + n, " %s", suffix);

  free (value);
  return buff;
}

int pcb_sprintf(char *string, const char *fmt, ...)
{
  char buf[255];
  char buf2[255];
  char *pbuf;

  va_list args;
  va_start(args, fmt);

  while(*fmt)
    {
      bool b_suffix   = 0;
      int  multiplier = 0;

      if(*fmt == '%')
        {
          char *unit_str = NULL;
          const char *ext_unit;
          BDimension value[2];
          int jump = 0;
          int count, i;

          ++fmt;
          pbuf = buf;
          buf[0] = '\0';
          /* Get printf sub-specifiers */
          while(isdigit(*fmt) || *fmt == '.' || *fmt == ' ' || *fmt == '*'
                              || *fmt == '#' || *fmt == 'l' || *fmt == 'L'
                              || *fmt == 'h')
            *pbuf++ = *fmt++;
          *pbuf = '\0';
          /* Get our sub-specifiers */
          if(*fmt == ':')
            {
              multiplier = strtol(fmt + 1, (char **)&fmt, 0);
            }
          if(*fmt == '$')
            {
              b_suffix = 1;
              fmt++;
            }
          /* Handle format */
          switch(*fmt)
            {
            /* Printf specs */
            case 'o': case 'i': case 'd':
            case 'u': case 'x': case 'X':
              sprintf(buf2, "%%%s%c%%n", buf, *fmt);
              sprintf(string, buf2, va_arg(args, int), &jump);
              break;
            case 'e': case 'E': case 'f':
            case 'g': case 'G':
              sprintf(buf2, "%%%s%c%%n", buf, *fmt);
              if (strchr (buf, '*'))
                sprintf(string, buf2, va_arg(args, double), &jump);
              else
                sprintf(string, buf2, va_arg(args, double), va_arg(args, int), &jump);
              break;
            case 'c':
              sprintf(buf2, "%%%s%c%%n", buf, *fmt);
              if(buf[0] == 'l' && sizeof(int) <= sizeof(wchar_t))
                {
                  sprintf(string, buf2, va_arg(args, wchar_t), &jump);
                }
              else
                {
                  sprintf(string, buf2, va_arg(args, int), &jump);
                }
            case 's':
              sprintf(buf2, "%%%s%c%%n", buf, *fmt);
              if(buf[0] == 'l')
                {
                  sprintf(string, buf2, va_arg(args, wchar_t *), &jump);
                }
              else
                {
                  sprintf(string, buf2, va_arg(args, char *), &jump);
                }
              break;
            case 'n':
              sprintf(buf2, "%%%s%c%%n", buf, *fmt);
              sprintf(string, buf2, va_arg(args, int *), &jump);
              break;
            case 'p':
              sprintf(buf2, "%%%s%c%%n", buf, *fmt);
              sprintf(string, buf2, va_arg(args, void *), &jump);
              break;
            case '%':
              *string++ = *fmt;
              break;
            /* Our specs */
            case 'm':
              ++fmt;
              if (*fmt == '*')
                ext_unit = va_arg(args, const char *);
              value[0] = va_arg(args, BDimension);
              count = 1;
              switch(*fmt)
                {
                case 's': unit_str = CoordsToString(value, 1, buf, ALLOW_MM | ALLOW_MIL, b_suffix); break;
                case 'S': unit_str = CoordsToString(value, 1, buf, ALLOW_ALL, b_suffix); break;
                case 'M': unit_str = CoordsToString(value, 1, buf, ALLOW_METRIC, b_suffix); break;
                case 'L': unit_str = CoordsToString(value, 1, buf, ALLOW_IMPERIAL, b_suffix); break;
                case 'r': unit_str = CoordsToString(value, 1, buf, ALLOW_READABLE, b_suffix); break;
                case '9': value[count++] = va_arg(args, BDimension);
                case '8': value[count++] = va_arg(args, BDimension);
                case '7': value[count++] = va_arg(args, BDimension);
                case '6': value[count++] = va_arg(args, BDimension);
                case '5': value[count++] = va_arg(args, BDimension);
                case '4': value[count++] = va_arg(args, BDimension);
                case '3': value[count++] = va_arg(args, BDimension);
                case 'D':
                  value[count++] = va_arg(args, BDimension);
                  unit_str = CoordsToString(value, count, buf, ALLOW_ALL, b_suffix);
                  break;
                case 'd':
                  value[1] = va_arg(args, BDimension);
                  unit_str = CoordsToString(value, 2, buf, ALLOW_MM | ALLOW_MIL, b_suffix);
                  break;
                case '*':
                  for (i = 0; i < N_UNITS; ++i)
                    if (strcmp (ext_unit, Units[i].suffix) == 0)
                      unit_str = CoordsToString(value, 1, buf, Units[i].allow, b_suffix);
                  if (unit_str == NULL)
                    unit_str = CoordsToString(value, 1, buf, ALLOW_ALL, b_suffix);
                  break;
                default:
                  for (i = 0; i < N_UNITS; ++i)
                    if (*fmt == Units[i].printf_code)
                      unit_str = CoordsToString(value, 1, buf, Units[i].allow, b_suffix);
                  if (unit_str == NULL)
                    unit_str = CoordsToString(value, 1, buf, ALLOW_ALL, b_suffix);
                  break;
                }
              sprintf(string, "%s%n", unit_str, &jump);
              free(unit_str);
              break;
            }
          string += jump;
        }
      else
        *string++ = *fmt;
      ++fmt;
    }
  *string = 0;

  va_end(args);
  return 0;
}


/* TEST DRIVER */
int main(void)
{
  char buff[200];
  BDimension d;

  for(d = 10; d < 100; d += 20)
    {
      pcb_sprintf(buff, "Inputted %$mD.", d, d + 5);
      puts (buff);
    }

  for(d = MM_TO_COORD(10); d < MM_TO_COORD(100); d += MM_TO_COORD(2.5))
    {
      pcb_sprintf(buff, "Inputted %$m*.", "in", (BDimension) d);
      puts (buff);
    }

  return 0;
}