/* printf.c - Format and Print the data.
 *
 * Copyright 2014 Sandeep Sharma <sandeep.jack2756@gmail.com>
 * Copyright 2014 Kyungwan Han <asura321@gmail.com>
 *
 * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html

USE_PRINTF(NEWTOY(printf, "<1", TOYFLAG_USR|TOYFLAG_BIN))

config PRINTF 
    bool "printf"
    default n
    help
    usage: printf Format [Arguments..]
    
    Format and print ARGUMENT(s) according to FORMAT.
    Format is 'C' control output as 'C' printf.
*/
#define FOR_printf
#include "toys.h"

GLOBALS(
  char *hv_w;
  char *hv_p;
  int encountered;
)

// Calculate width and precision from format string
static int get_w_p()
{
  char *ptr, *str = *toys.optargs;
  
  errno = 0;
  if (*str == '-') str++;
  long value = strtol(str, &ptr, 10);
  if (errno || (ptr && (*ptr != '\0' || ptr == str)))
    perror_msg("Invalid num %s", *toys.optargs);
  if (*--str == '-') return (int)(-1 * value);
  return value;
}

// Add ll and L to Interger and floating point formats respectively.
static char *get_format(char *f)
{
  int len = strlen(f);
  char last = f[--len], *post = "";

  f[len] = '\0';
  if (strchr("diouxX", last)) post = "ll";  // add ll to integer modifier.
  else if (strchr("feEgG", last)) post = "L"; // add L to float modifier.
  return xmprintf("%s%s%c", f, post, last);
}

// Print arguments with corresponding conversion and width and precision.
static void print(char *fmt, int w, int p, int l)
{
  char *ptr = (fmt+l-1), *ep = NULL, *format = NULL;
  long long val;
  long double dval;
  
  errno = 0;
  switch (*ptr) {
    case 'd': /*FALL_THROUGH*/
    case 'i': /*FALL_THROUGH*/
    case 'o': /*FALL_THROUGH*/
    case 'u': /*FALL_THROUGH*/
    case 'x': /*FALL_THROUGH*/
    case 'X':
      if (!*toys.optargs) val = 0;
      else {
        if (**toys.optargs == '\'' || **toys.optargs == '"')
          val = *((*toys.optargs) + 1);
        else {
          val = strtoll(*toys.optargs, &ep, 0);
          if (errno || (ep && (*ep != '\0' || ep == *toys.optargs))) {
            perror_msg("Invalid num %s", *toys.optargs);
            val = 0;
          }
        }
      }
      format = get_format(fmt);
      TT.hv_w ? (TT.hv_p ? printf(format, w, p, val) : printf(format, w, val))
        : (TT.hv_p ? printf(format, p, val) : printf(format, val));
      break;
    case 'g': /*FALL_THROUGH*/
    case 'e': /*FALL_THROUGH*/
    case 'E': /*FALL_THROUGH*/
    case 'G': /*FALL_THROUGH*/
    case 'f':
      if (*toys.optargs) {
        dval = strtold(*toys.optargs, &ep);
        if (errno || (ep && (*ep != '\0' || ep == *toys.optargs))) {
          perror_msg("Invalid num %s", *toys.optargs);
          dval = 0;
        }
      } else dval = 0;
      format = get_format(fmt);
      TT.hv_w ? (TT.hv_p ? printf(format, w, p, dval):printf(format, w, dval))
        : (TT.hv_p ? printf(format, p, dval) :  printf(format, dval));
      break;
    case 's':
      {
        char *str = (*toys.optargs ? *toys.optargs : "");
        TT.hv_w ? (TT.hv_p ? printf(fmt,w,p,str): printf(fmt, w, str))
          : (TT.hv_p ? printf(fmt, p, str) : printf(fmt, str));
      }
      break;
    case 'c':
      printf(fmt, (*toys.optargs ? **toys.optargs : '\0'));
      break;
  }
  if (format) free(format);
}

// Handle the escape sequences.
static int handle_slash(char **esc_val)
{
  char *ptr = *esc_val;
  int esc_length = 0;
  unsigned  base = 0, num = 0, result = 0, count = 0;
  
  /*
   * Hex escape sequence have only 1 or 2 digits, xHH. Oct escape sequence 
   * have 1,2 or 3 digits, xHHH. Leading "0" (\0HHH) we are ignoring.
   */
  if (*ptr == 'x') {
    ptr++;
    esc_length++;
    base = 16;
  } else if (isdigit(*ptr)) base = 8;

  while (esc_length < 3 && base) {
    num = tolower(*ptr) - '0';
    if (num > 10) num += ('0' - 'a' + 10);
    if (num >= base) {
      if (base == 16) {
        esc_length--;
        if (!esc_length) {// Invalid hex value eg. /xvd, print as it is /xvd
          result = '\\';
          ptr--;
        }
      }
      break;
    }
    esc_length++;
    count = result = (count * base) + num;
    ptr++;
  }
  if (base) {
    ptr--;
    *esc_val = ptr;
    return (char)result;
  } else {
    switch (*ptr) {
      case 'n':  result = '\n'; break;
      case 't':  result = '\t'; break;
      case 'e':  result = (char)27; break;
      case 'b':  result = '\b'; break;
      case 'a':  result = '\a'; break;
      case 'f':  result = '\f'; break;
      case 'v':  result = '\v'; break;
      case 'r':  result = '\r'; break;
      case '\\': result = '\\'; break;
      default :
        result = '\\';
        ptr--; // Let pointer pointing to / we will increment after returning.
        break;
    }
  }
  *esc_val = ptr;
  return (char)result;
}

// Handle "%b" option with '\' interpreted.
static void print_esc_str(char *str)              
{
  for (; *str; str++) {
    if (*str == '\\') {
      str++;
      xputc(handle_slash(&str)); //print corresponding char
    } else xputc(*str);
  }
}

// Prase the format string and print.
static void parse_print(char *f)
{
  char *start, *p, format_specifiers[] = "diouxXfeEgGcs";
  int len = 0, width = 0, prec = 0;

  while (*f) {
    switch (*f) {
      case '%':
        start = f++;
        len++;
        if (*f == '%') {
          xputc('%');
          break;
        }
        if (*f == 'b') {
          if (*toys.optargs) {
            print_esc_str(*toys.optargs++);
            TT.encountered = 1;
          } else print_esc_str("");
          break;
        }
        if (strchr("-+# ", *f)) f++, len++;
        if (*f == '*') {
          f++, len++;
          if (*toys.optargs) {
            width = get_w_p();
            toys.optargs++;
          }
        } else {
          while (isdigit(*f)) f++, len++;
        }
        if (*f == '.') {
          f++, len++;
          if (*f == '*') {
            f++, len++;
            if (*toys.optargs) {
              prec = get_w_p();
              toys.optargs++;
            }
          } else {
            while (isdigit(*f)) f++, len++;
          }
        }
        if (!(p = strchr(format_specifiers, *f)))
          perror_exit("Missing OR Invalid format specifier");
        else {
          len++;
          TT.hv_p = strstr(start, ".*");
          TT.hv_w = strchr(start, '*');
          //pitfall: handle diff b/w * and .*
          if ((TT.hv_w-1) == TT.hv_p) TT.hv_w = NULL;
          memcpy((p = xzalloc(len+1)), start, len);
          print(p, width, prec, len);
          if (*toys.optargs) toys.optargs++;
          free(p);
          p = NULL;
        } 
        TT.encountered = 1;
        break;
      case '\\':
        if (f[1]) {
          if (*++f == 'c') exit(0); //Got '\c', so no further output  
          xputc(handle_slash(&f));
        } else xputc(*f);
        break;
      default:
        xputc(*f);
        break;
    }
    f++;
    len = 0;
  }
}

void printf_main(void)
{
  char *format = *toys.optargs++;

  TT.encountered = 0;
  parse_print(format); //printf acc. to format.
  //Re-use FORMAT arg as necessary to convert all given ARGS.
  while (*toys.optargs && TT.encountered) parse_print(format);
  xflush();
}