/*
 *	CUPS Filter Generating XCPT
 *
 *	(c) 2016 Martin Mares <mj@ucw.cz>
 */

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

#define PRINTF(i,j) __attribute__((format(printf,i,j)))
#define NONRET __attribute__((noreturn))

#if 1
#define DEBUG(...) debug(__VA_ARGS__)
#else
#define DEBUG(...) do { } while(0)
#endif

/*** Utility functions ***/

static void PRINTF(1,2) NONRET bug(const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  fprintf(stderr, "ERROR: XCPT: ");
  vfprintf(stderr, fmt, args);
  fputc('\n', stderr);

  va_end(args);
  exit(1);
}

#define ASSERT(_cond) do { if (!(_cond)) bug("Assertion failed: %s", #_cond); } while (0)

#if 0
static void PRINTF(1,2) error(const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  fprintf(stderr, "ERROR: XCPT: ");
  vfprintf(stderr, fmt, args);
  fputc('\n', stderr);

  va_end(args);
}
#endif

static void PRINTF(1,2) debug(const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  fprintf(stderr, "DEBUG: XCPT: ");
  vfprintf(stderr, fmt, args);
  fputc('\n', stderr);

  va_end(args);
}

static char *xstrdup(const char *x)
{
  char *p = strdup(x);
  if (!p)
    bug("Out of memory");
  return p;
}

/*** Options ***/

#define OPTIONS \
	O(JOB_ID, "?") \
	O(USERNAME, "?") \
	O(JOB_TITLE, "?") \
	O(COPIES, "1") \
	O(LANGUAGE, "?") \
	O(COLOR, "color") \
	O(ORIENTATION, "") \
	O(PRIVATE, "") \
	O(OFFSET, "none") \
	O(SIDES, "one-sided") \
	O(COLLATE, "uncollated") \
	O(OUTPUT, "automatic") \
	O(MEDIACOLOR, "white") \
	O(INPUTTRAY, "automatic") \
	O(TRAYFEED, "stack") \
	O(MEDIATYPE, "system-default") \
	O(MEDIAXSIZE, "595") \
	O(MEDIAYSIZE, "842") \
	O(FINISH_STAPLE, "") \
	O(FINISH_PUNCH, "") \
	O(FINISH_FOLD, "") \
	O(FEED_ORIENTATION, "automatic") \
	O(QUALITY, "standard")

enum option {
#define O(name, val) OPT_##name,
  OPTIONS
#undef O
  OPT_MAX
};

const char *opt_names[] = {
#define O(name, val) #name,
  OPTIONS
#undef O
};

char *opt[] = {
#define O(name, val) [OPT_##name] = val,
  OPTIONS
#undef O
};

#define OPT(name) opt[OPT_##name]
#define OPT_INT(name) atoi(opt[OPT_##name])
#define OPT_IS(name, val) !strcasecmp(OPT(name), val)
#define OPT_IS_SET(name) *OPT(name)
#define OPT_IS_TRUE(name) OPT_IS(name, "yes")

static void set_option(char *key, char *val)
{
  for (int i=0; i<OPT_MAX; i++)
    if (!strcmp(opt_names[i], key))
      {
	debug("Setting %s=<%s>", key, val);
	opt[i] = xstrdup(val);
	return;
      }
  debug("Unrecognized option <%s>", key);
}

/**
 *  This is our parser of PJL. A hacky one, indeed, but it is expected
 *  to parse only PJL directives generated by our PPD or by CUPS itself,
 *  so we need not pay attention to all obscure details of the language.
 **/

#define LINESIZE 256

static char *skip_spaces(char *p)
{
  while (*p == ' ' || *p == '\t')
    p++;
  return p;
}

static int my_toupper(int c)
{
  if (c >= 'a' && c <= 'z')
    return c - 32;
  else
    return c;
}

static char *token_buf;

static char *get_token(char **pp)
{
  char *token_start = token_buf;
  char *tok = token_start;
  char *p = skip_spaces(*pp);
  if (!*p)
    return NULL;
  int c = my_toupper(*p);
  if (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_')
    {
      while (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_')
	{
	  *tok++ = c;
	  c = my_toupper(*++p);
	}
    }
  else
    *tok++ = *p++;
  *tok++ = 0;
  *pp = p;
  token_buf = tok;
  return token_start;
}

static void parse_pjl(void)
{
  static const char uel[] = "\e%-12345X";
  for (int i=0; uel[i]; i++)
    if (getchar() != uel[i])
      bug("PJL error: No UEL found (pos %u)", i);

  char line[LINESIZE], tokens[2*LINESIZE];
  for (;;)
    {
      int i = 0;
      for (;;)
	{
	  int c = getchar();
	  if (c < 0)
	    bug("PJL error: Premature EOF");
	  if (i < 4 && c != "@PJL"[i])
	    bug("PJL error: Unrecognized line");
	  if (c == '\r')
	    continue;
	  if (c == '\n')
	      break;
	  if (i >= LINESIZE-1)
	    bug("PJL error: Line too long");
	  line[i++] = c;
	}
      while (i > 0 && (line[i-1] == ' ' || line[i-1] == '\t'))
	i--;
      line[i] = 0;

      DEBUG("PJL: %s", line);
      char *p = line+1;
      token_buf = tokens;
      char *t, *t2, *t3;
      if (!(t = get_token(&p)) || strcasecmp(t, "PJL"))
	bug("PJL error: Malformed line");
      if (!(t = get_token(&p)))
	continue;

      if (!strcasecmp(t, "ENTER"))
	{
	  if ((t2 = get_token(&p)) && !strcasecmp(t2, "LANGUAGE"))
	    {
	      if ((t3 = get_token(&p)) && !strcasecmp(t3, "="))
		{
		  set_option("LANGUAGE", skip_spaces(p));
		  return;
		}
	    }
	}

      if (!strcasecmp(t, "SET"))
	{
	  if (t2 = get_token(&p))
	    {
	      if ((t3 = get_token(&p)) && !strcasecmp(t3, "="))
		{
		  p = skip_spaces(p);
		  if (*p == '"')
		    {
		      *p++ = 0;
		      char *q = p;
		      while (*q && *q != '"')
			q++;
		      if (*q)
			*q = 0;
		    }
		  set_option(t2, p);
		}
	    }
	}
    }
}

/*** XCPT generator ***/

#define STACK_DEPTH 10

static int xcpt_level;
static const char *xcpt_stack[STACK_DEPTH];

static void xcpt_start_line(void)
{
  printf("@PJL XCPT ");
  for (int i=0; i<xcpt_level; i++)
    putchar('\t');
}

static void xcpt_printf(const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  xcpt_start_line();
  vprintf(fmt, args);
  putchar('\n');

  va_end(args);
}

static void xcpt_push(const char *name)
{
  ASSERT(xcpt_level < STACK_DEPTH);
  xcpt_stack[xcpt_level++] = xstrdup(name);
}

static void xcpt_open(const char *name)
{
  xcpt_printf("<%s>", name);
  xcpt_push(name);
}

static void xcpt_close(void)
{
  ASSERT(xcpt_level);
  const char *name = xcpt_stack[--xcpt_level];
  xcpt_printf("</%s>", name);
  free((char *) name);
}

static void xcpt_integer(const char *name, int value)
{
  xcpt_printf("<%s syntax=\"integer\">%d</%s>", name, value, name);
}

static void xcpt_dimension(const char *name, int pt)
{
  /*
   *  We get dimensions in PostScript points (1/72 in).
   *  Xerox printers expect them in 100th's of mm, but
   *  last two digits must be 0.
   */
  double m = pt * (25.4 / 72.) + 0.5;
  xcpt_integer(name, 100 * (int)m);
}

static void xcpt_keyword(const char *name, const char *value)
{
  xcpt_printf("<%s syntax=\"keyword\">%s</%s>", name, value, name);
}

static void xcpt_enum(const char *name, int value)
{
  xcpt_printf("<%s syntax=\"enum\">%d</%s>", name, value, name);
}

static void xcpt_media_type(const char *name, const char *value)
{
  xcpt_printf("<%s syntax=\"mimeMediaType\">%s</%s>", name, value, name);
}

static void xcpt_collection_open(const char *name)
{
  xcpt_printf("<%s syntax=\"collection\">", name);
  xcpt_push(name);
}

static void xcpt_set_open(const char *name)
{
  xcpt_printf("<%s syntax=\"1setOf\">", name);
  xcpt_push(name);
}

static void xcpt_cdata(const char *text)
{
  int c;
  while (c = *text++)
    switch (c)
      {
      case '<':
	printf("&lt;");
	break;
      case '>':
	printf("&gt;");
	break;
      case '&':
	printf("&amp;");
	break;
      case '"':
	printf("&quot;");
	break;
      default:
	putchar(c);
      }
}

static void xcpt_text(const char *name, const char *text)
{
  xcpt_start_line();
  printf("<%s syntax=\"text\" xml:space=\"preserve\">", name);
  xcpt_cdata(text);
  printf("</%s>\n", name);
}

static void xcpt_name(const char *name, const char *text)
{
  xcpt_start_line();
  printf("<%s syntax=\"name\" xml:space=\"preserve\">", name);
  xcpt_cdata(text);
  printf("</%s>\n", name);
}

static void gen_finishings(const char *opt)
{
  int fin, n;
  while (sscanf(opt, "%d%n", &fin, &n) > 0)
    {
      xcpt_enum("value", fin);
      opt += n;
    }
}

static void gen_job_template(void)
{
  // Color adjustments
  xcpt_integer("adjust-contrast", 0);		// FIXME: all of them
  xcpt_integer("adjust-cyan-red", 0);
  xcpt_integer("adjust-lightness", 0);
  xcpt_integer("adjust-magenta-green", 0);
  xcpt_integer("adjust-saturation", 0);
  xcpt_integer("adjust-yellow-blue", 0);
  if (OPT_IS(COLOR, "color"))
    {
      xcpt_keyword("color-adjustment-set", "automatic");
      xcpt_keyword("color-effects-type", "color");
    }
  else
    {
      xcpt_keyword("color-adjustment-set", "monochrome-grayscale");
      xcpt_keyword("color-effects-type", "monochrome-grayscale");
    }

  xcpt_integer("copies", OPT_INT(COPIES));

  if (OPT_IS_SET(ORIENTATION))
    xcpt_keyword("document-reading-orientation", OPT(ORIENTATION));

  // XCPT specs allow feed-orientation either globaly, or inside media-col.
  // The latter makes more sense, but it does not work on the WC7845.
  xcpt_keyword("feed-orientation", OPT(FEED_ORIENTATION));

  xcpt_set_open("finishings");
  if (OPT_IS_SET(FINISH_FOLD))
    gen_finishings(OPT(FINISH_FOLD));
  else if (OPT_IS_SET(FINISH_STAPLE) || OPT_IS_SET(FINISH_PUNCH))
    {
      gen_finishings(OPT(FINISH_STAPLE));
      gen_finishings(OPT(FINISH_PUNCH));
    }
  else
    xcpt_enum("value", 3);
  xcpt_close();

  xcpt_keyword("halftone-text", "ignore-pdl");
  xcpt_keyword("halftone-graphics", "ignore-pdl");
  xcpt_keyword("halftone-images", "ignore-pdl");

  if (OPT_IS_SET(PRIVATE))
    xcpt_keyword("hold-for-authentication", "user-id");
  else
    xcpt_keyword("hold-for-authentication", "none");

  // Interleaving not supported at the moment
  xcpt_collection_open("interleaved-sheets-col");
  xcpt_keyword("interleaved-sheets-type", "none");
  xcpt_close();

  xcpt_set_open("job-offset");
  xcpt_keyword("value", OPT(OFFSET));
  xcpt_close();

  // Saved jobs are not supported at the moment
  xcpt_collection_open("job-save-disposition");
  xcpt_keyword("save", "none");
  xcpt_close();

  // We leave printing of banner sheets to CUPS itself
  xcpt_keyword("job-sheets", "none");

  xcpt_collection_open("client-default-attributes-col");
    xcpt_collection_open("media-col");
    xcpt_keyword("input-tray", OPT(INPUTTRAY));
    xcpt_keyword("tray-feed", OPT(TRAYFEED));
    xcpt_keyword("media-color", OPT(MEDIACOLOR));
    xcpt_collection_open("media-size");
      xcpt_dimension("x-dimension", OPT_INT(MEDIAXSIZE));
      xcpt_dimension("y-dimension", OPT_INT(MEDIAYSIZE));
      xcpt_close();
    xcpt_keyword("media-type", OPT(MEDIATYPE));
    xcpt_close();
  xcpt_keyword("print-quality-level", OPT(QUALITY));
  xcpt_keyword("sides", OPT(SIDES));
  xcpt_close();

  xcpt_keyword("output-bin", OPT(OUTPUT));
  xcpt_keyword("sheet-collate", OPT(COLLATE));

  // Rendering intents and color sources: so far, nothing can be set
  xcpt_keyword("embedded-profiles", "automatic");
  xcpt_keyword("print-settings", "none");
  xcpt_keyword("rendering-intent-graphics", "automatic");
  xcpt_keyword("rendering-intent-images", "automatic");
  xcpt_keyword("rendering-intent-text", "automatic");
  xcpt_keyword("spot-color-mapping", "use-local-printer-values");
  xcpt_keyword("undefined-source-cmyk-images", "automatic");
  xcpt_keyword("undefined-source-cmyk-text", "automatic");
  xcpt_keyword("undefined-source-gray-images", "automatic");
  xcpt_keyword("undefined-source-gray-text", "automatic");
  xcpt_keyword("undefined-source-rgb-images", "automatic");
  xcpt_keyword("undefined-source-rgb-text", "automatic");
  xcpt_keyword("undefined-spot-color-images", "automatic");
  xcpt_keyword("undefined-spot-color-text", "automatic");
}

static void gen_job_operation(void)
{
  xcpt_keyword("creator-name-attributes", "cups-xcpt");
  xcpt_keyword("creator-name-pdl", "unknown");
  xcpt_text("creator-version-attributes", "0.1");	// FIXME
  xcpt_text("creator-version-pdl", "0.0");
  if (OPT_IS(LANGUAGE, "PDF"))
    xcpt_media_type("document-format", "application/pdf");
  else
    xcpt_media_type("document-format", "application/postscript");
  xcpt_name("job-id-from-client", OPT(JOB_ID));
  xcpt_name("job-name", OPT(JOB_TITLE));
  xcpt_name("job-originating-user-domain", "KAM");	// FIXME

  const char *user = OPT(USERNAME);
  xcpt_name("job-originating-user-name", user);
  xcpt_name("requesting-user-name", user);
}

static void gen_xcpt(void)
{
  printf("\e%%-12345X@PJL JOB\n");
  xcpt_printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
  xcpt_printf("<!DOCTYPE xpif SYSTEM \"xpif-v02081.dtd\">");
  xcpt_printf("<xpif version=\"1.0\" cpss-version=\"2.07\" xml:lang=\"en-US\">");
  xcpt_push("xpif");

  xcpt_open("job-template-attributes");
  gen_job_template();
  xcpt_close();

  xcpt_open("job-operation-attributes");
  gen_job_operation();
  xcpt_close();

  xcpt_close();
  ASSERT(!xcpt_level);
  printf("@PJL ENTER LANGUAGE = %s\n", OPT(LANGUAGE));
}

static void copy_body(void)
{
  char buf[1024];
  size_t n;

  while (n = fread(buf, 1, sizeof(buf), stdin))
    {
      if (fwrite(buf, 1, n, stdout) != n)
	bug("Write error");
    }
}

/*** Main ***/

int main(int argc, char **argv)
{
  if (argc < 5)
    {
      fprintf(stderr, "Usage: xcpt <job> <user> <title> <num-copies> [<options>]\n");
      return 1;
    }
  set_option("JOB_ID", argv[1]);
  set_option("USERNAME", argv[2]);
  set_option("JOB_TITLE", argv[3]);
  set_option("COPIES", argv[4]);

  parse_pjl();

  for (int i=0; i<OPT_MAX; i++)
    DEBUG("Option %s=<%s>", opt_names[i], opt[i]);

  gen_xcpt();
  copy_body();
  return 0;
}