/* Copyright (C) 1994-2020 Free Software Foundation, Inc.
     Written by James Clark (jjc@jclark.com)

This file is part of groff.

groff is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or
(at your option) any later version.

groff is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>. */

/*
TODO

option to use beziers for circle/ellipse/arc
option to use lines for spline (for LJ3)
left/top offset registration
output bin selection option
paper source option
output non-integer parameters using fixed point numbers
X command to insert contents of file
X command to specify inline escape sequence (how to specify unprintable chars?)
X command to include bitmap graphics
*/

#include <assert.h>

#include "driver.h"
#include "nonposix.h"

extern "C" const char *Version_string;

static struct {
  const char *name;
  int code;
  // at 300dpi
  int x_offset_portrait;
  int x_offset_landscape;
} paper_table[] = {
  { "letter", 2, 75, 60 },
  { "legal", 3, 75, 60 },
  { "executive", 1, 75, 60 },
  { "a4", 26, 71, 59 },
  { "com10", 81, 75, 60 },
  { "monarch", 80, 75, 60 },
  { "c5", 91, 71, 59 },
  { "b5", 100, 71, 59 },
  { "dl", 90, 71, 59 },
};

static int user_paper_size = -1;
static int landscape_flag = 0;
static int duplex_flag = 0;

// An upper limit on the paper dimensions in centipoints,
// used for setting HPGL picture frame.
#define MAX_PAPER_WIDTH (12*720)
#define MAX_PAPER_HEIGHT (17*720)

// Dotted lines that are thinner than this don't work right.
#define MIN_DOT_PEN_WIDTH .351

#ifndef DEFAULT_LINE_WIDTH_FACTOR
// in ems/1000
#define DEFAULT_LINE_WIDTH_FACTOR 40
#endif

const int DEFAULT_HPGL_UNITS = 1016;
int line_width_factor = DEFAULT_LINE_WIDTH_FACTOR;
unsigned ncopies = 0;		// 0 means don't send ncopies command

static int lookup_paper_size(const char *);

class lj4_font : public font {
public:
  ~lj4_font();
  void handle_unknown_font_command(const char *command, const char *arg,
				   const char *filename, int lineno);
  static lj4_font *load_lj4_font(const char *);
  int weight;
  int style;
  int proportional;
  int typeface;
private:
  lj4_font(const char *);
};

lj4_font::lj4_font(const char *nm)
: font(nm), weight(0), style(0), proportional(0), typeface(0)
{
}

lj4_font::~lj4_font()
{
}

lj4_font *lj4_font::load_lj4_font(const char *s)
{
  lj4_font *f = new lj4_font(s);
  if (!f->load()) {
    delete f;
    return 0;
  }
  return f;
}

static struct {
  const char *s;
  int lj4_font::*ptr;
  int min;
  int max;
} command_table[] = {
  { "pclweight", &lj4_font::weight, -7, 7 },
  { "pclstyle", &lj4_font::style, 0, 32767 },
  { "pclproportional", &lj4_font::proportional, 0, 1 },
  { "pcltypeface", &lj4_font::typeface, 0, 65535 },
};

void lj4_font::handle_unknown_font_command(const char *command,
					   const char *arg,
					   const char *filename, int lineno)
{
  for (unsigned int i = 0;
       i < sizeof(command_table)/sizeof(command_table[0]); i++) {
    if (strcmp(command, command_table[i].s) == 0) {
      if (arg == 0)
	fatal_with_file_and_line(filename, lineno,
				 "'%1' command requires an argument",
				 command);
      char *ptr;
      long n = strtol(arg, &ptr, 10);
      if (n == 0 && ptr == arg)
	fatal_with_file_and_line(filename, lineno,
				 "'%1' command requires numeric argument",
				 command);
      if (n < command_table[i].min) {
	error_with_file_and_line(filename, lineno,
				 "argument for '%1' command must not be less than %2",
				 command, command_table[i].min);
	n = command_table[i].min;
      }
      else if (n > command_table[i].max) {
	error_with_file_and_line(filename, lineno,
				 "argument for '%1' command must not be greater than %2",
				 command, command_table[i].max);
	n = command_table[i].max;
      }
      this->*command_table[i].ptr = int(n);
      break;
    }
  }
}

class lj4_printer : public printer {
public:
  lj4_printer(int);
  ~lj4_printer();
  void set_char(glyph *, font *, const environment *, int, const char *name);
  void draw(int code, int *p, int np, const environment *env);
  void begin_page(int);
  void end_page(int page_length);
  font *make_font(const char *);
  void end_of_line();
private:
  void set_line_thickness(int size, int dot = 0);
  void hpgl_init();
  void hpgl_start();
  void hpgl_end();
  int moveto(int hpos, int vpos);
  int moveto1(int hpos, int vpos);

  int cur_hpos;
  int cur_vpos;
  lj4_font *cur_font;
  int cur_size;
  unsigned short cur_symbol_set;
  int x_offset;
  int line_thickness;
  double pen_width;
  double hpgl_scale;
  int hpgl_inited;
  int paper_size;
};

inline
int lj4_printer::moveto(int hpos, int vpos)
{
  if (cur_hpos != hpos || cur_vpos != vpos || cur_hpos < 0)
    return moveto1(hpos, vpos);
  else
    return 1;
}

inline
void lj4_printer::hpgl_start()
{
  fputs("\033%1B", stdout);
}

inline
void lj4_printer::hpgl_end()
{
  fputs(";\033%0A", stdout);
}

lj4_printer::lj4_printer(int ps)
: cur_hpos(-1),
  cur_font(0),
  cur_size(0),
  cur_symbol_set(0),
  line_thickness(-1),
  pen_width(-1.0),
  hpgl_inited(0)
{
  if (7200 % font::res != 0)
    fatal("invalid resolution %1: resolution must be a factor of 7200",
	  font::res);
  fputs("\033E", stdout);		// reset
  if (font::res != 300)
    printf("\033&u%dD", font::res);	// unit of measure
  if (ncopies > 0)
    printf("\033&l%uX", ncopies);
  paper_size = 0;		// default to letter
  if (font::papersize) {
    int n = lookup_paper_size(font::papersize);
    if (n < 0)
      error("ignoring invalid paper format '%1'", font::papersize);
    else
      paper_size = n;
  }
  if (ps >= 0)
    paper_size = ps;
  printf("\033&l%dA"		// paper format
	 "\033&l%dO"		// orientation
	 "\033&l0E",		// no top margin
	 paper_table[paper_size].code,
	 landscape_flag != 0);
  if (landscape_flag)
    x_offset = paper_table[paper_size].x_offset_landscape;
  else
    x_offset = paper_table[paper_size].x_offset_portrait;
  x_offset = (x_offset * font::res) / 300;
  if (duplex_flag)
     printf("\033&l%dS", duplex_flag);
}

lj4_printer::~lj4_printer()
{
  fputs("\033E", stdout);
}

void lj4_printer::begin_page(int)
{
}

void lj4_printer::end_page(int)
{
  putchar('\f');
  cur_hpos = -1;
}

void lj4_printer::end_of_line()
{
  cur_hpos = -1;		// force absolute motion
}

inline
int is_unprintable(unsigned char c)
{
  return c < 32 && (c == 0 || (7 <= c && c <= 15) || c == 27);
}

void lj4_printer::set_char(glyph *g, font *f, const environment *env,
			   int w, const char *)
{
  int code = f->get_code(g);

  unsigned char ch = code & 0xff;
  unsigned short symbol_set = code >> 8;
  if (symbol_set != cur_symbol_set) {
    printf("\033(%d%c", symbol_set/32, (symbol_set & 31) + 64);
    cur_symbol_set = symbol_set;
  }
  if (f != cur_font) {
    lj4_font *psf = (lj4_font *)f;
    // FIXME only output those that are needed
    printf("\033(s%dp%ds%db%dT",
	   psf->proportional,
	   psf->style,
	   psf->weight,
	   psf->typeface);
    if (!psf->proportional || !cur_font || !cur_font->proportional)
      cur_size = 0;
    cur_font = psf;
  }
  if (env->size != cur_size) {
    if (cur_font->proportional) {
      static const char *quarters[] = { "", ".25", ".5", ".75" };
      printf("\033(s%d%sV", env->size/4, quarters[env->size & 3]);
    }
    else {
      double pitch = double(font::res)/w;
      // PCL uses the next largest pitch, so round it down.
      pitch = floor(pitch*100.0)/100.0;
      printf("\033(s%.2fH", pitch);
    }
    cur_size = env->size;
  }
  if (!moveto(env->hpos, env->vpos))
    return;
  if (is_unprintable(ch))
    fputs("\033&p1X", stdout);
  putchar(ch);
  cur_hpos += w;
}

int lj4_printer::moveto1(int hpos, int vpos)
{
  if (hpos < x_offset || vpos < 0)
    return 0;
  fputs("\033*p", stdout);
  if (cur_hpos < 0)
    printf("%dx%dY", hpos - x_offset, vpos);
  else {
    if (cur_hpos != hpos)
      printf("%s%d%c", hpos > cur_hpos ? "+" : "",
	     hpos - cur_hpos, vpos == cur_vpos ? 'X' : 'x');
    if (cur_vpos != vpos)
      printf("%s%dY", vpos > cur_vpos ? "+" : "", vpos - cur_vpos);
  }
  cur_hpos = hpos;
  cur_vpos = vpos;
  return 1;
}

void lj4_printer::draw(int code, int *p, int np, const environment *env)
{
  switch (code) {
  case 'R':
    {
      if (np != 2) {
	error("2 arguments required for rule");
	break;
      }
      int hpos = env->hpos;
      int vpos = env->vpos;
      int hsize = p[0];
      int vsize = p[1];
      if (hsize < 0) {
	hpos += hsize;
	hsize = -hsize;
      }
      if (vsize < 0) {
	vpos += vsize;
	vsize = -vsize;
      }
      if (!moveto(hpos, vpos))
	return;
      printf("\033*c%da%db0P", hsize, vsize);
      break;
    }
  case 'l':
    if (np != 2) {
      error("2 arguments required for line");
      break;
    }
    hpgl_init();
    if (!moveto(env->hpos, env->vpos))
      return;
    hpgl_start();
    set_line_thickness(env->size, p[0] == 0 && p[1] == 0);
    printf("PD%d,%d", p[0], p[1]);
    hpgl_end();
    break;
  case 'p':
  case 'P':
    {
      if (np & 1) {
	error("even number of arguments required for polygon");
	break;
      }
      if (np == 0) {
	error("no arguments for polygon");
	break;
      }
      hpgl_init();
      if (!moveto(env->hpos, env->vpos))
	return;
      hpgl_start();
      if (code == 'p')
	set_line_thickness(env->size);
      printf("PMPD%d", p[0]);
      for (int i = 1; i < np; i++)
	printf(",%d", p[i]);
      printf("PM2%cP", code == 'p' ? 'E' : 'F');
      hpgl_end();
      break;
    }
  case '~':
    {
      if (np & 1) {
	error("even number of arguments required for spline");
	break;
      }
      if (np == 0) {
	error("no arguments for spline");
	break;
      }
      hpgl_init();
      if (!moveto(env->hpos, env->vpos))
	return;
      hpgl_start();
      set_line_thickness(env->size);
      printf("PD%d,%d", p[0]/2, p[1]/2);
      const int tnum = 2;
      const int tden = 3;
      if (np > 2) {
	fputs("BR", stdout);
	for (int i = 0; i < np - 2; i += 2) {
	  if (i != 0)
	    putchar(',');
	  printf("%d,%d,%d,%d,%d,%d",
		 (p[i]*tnum)/(2*tden),
		 (p[i + 1]*tnum)/(2*tden),
		 p[i]/2 + (p[i + 2]*(tden - tnum))/(2*tden),
		 p[i + 1]/2 + (p[i + 3]*(tden - tnum))/(2*tden),
		 (p[i] - p[i]/2) + p[i + 2]/2,
		 (p[i + 1] - p[i + 1]/2) + p[i + 3]/2);
	}
      }
      printf("PR%d,%d", p[np - 2] - p[np - 2]/2, p[np - 1] - p[np - 1]/2);
      hpgl_end();
      break;
    }
  case 'c':
  case 'C':
    // troff adds an extra argument to C
    if (np != 1 && !(code == 'C' && np == 2)) {
      error("1 argument required for circle");
      break;
    }
    hpgl_init();
    if (!moveto(env->hpos + p[0]/2, env->vpos))
      return;
    hpgl_start();
    if (code == 'c') {
      set_line_thickness(env->size);
      printf("CI%d", p[0]/2);
    }
    else
      printf("WG%d,0,360", p[0]/2);
    hpgl_end();
    break;
  case 'e':
  case 'E':
    if (np != 2) {
      error("2 arguments required for ellipse");
      break;
    }
    hpgl_init();
    if (!moveto(env->hpos + p[0]/2, env->vpos))
      return;
    hpgl_start();
    printf("SC0,%.4f,0,-%.4f,2", hpgl_scale * double(p[0])/p[1], hpgl_scale);
    if (code == 'e') {
      set_line_thickness(env->size);
      printf("CI%d", p[1]/2);
    }
    else
      printf("WG%d,0,360", p[1]/2);
    printf("SC0,%.4f,0,-%.4f,2", hpgl_scale, hpgl_scale);
    hpgl_end();
    break;
  case 'a':
    {
      if (np != 4) {
	error("4 arguments required for arc");
	break;
      }
      hpgl_init();
      if (!moveto(env->hpos, env->vpos))
	return;
      hpgl_start();
      set_line_thickness(env->size);
      double c[2];
      if (adjust_arc_center(p, c)) {
	double sweep = ((atan2(p[1] + p[3] - c[1], p[0] + p[2] - c[0])
			 - atan2(-c[1], -c[0]))
			* 180.0/PI);
	if (sweep > 0.0)
	  sweep -= 360.0;
	printf("PDAR%d,%d,%f", int(c[0]), int(c[1]), sweep);
      }
      else
	printf("PD%d,%d", p[0] + p[2], p[1] + p[3]);
      hpgl_end();
    }
    break;
  case 'f':
    if (np != 1 && np != 2) {
      error("1 argument required for fill");
      break;
    }
    hpgl_init();
    hpgl_start();
    if (p[0] >= 0 && p[0] <= 1000)
      printf("FT10,%d", p[0]/10);
    hpgl_end();
    break;
  case 'F':
    // not implemented yet
    break;
  case 't':
    {
      if (np == 0) {
	line_thickness = -1;
      }
      else {
	// troff gratuitously adds an extra 0
	if (np != 1 && np != 2) {
	  error("0 or 1 argument required for thickness");
	  break;
	}
	line_thickness = p[0];
      }
      break;
    }
  default:
    error("unrecognised drawing command '%1'", char(code));
    break;
  }
}

void lj4_printer::hpgl_init()
{
  if (hpgl_inited)
    return;
  hpgl_inited = 1;
  hpgl_scale = double(DEFAULT_HPGL_UNITS)/font::res;
  printf("\033&f0S"		// push position
	 "\033*p0x0Y"		// move to 0,0
	 "\033*c%dx%dy0T"	// establish picture frame
	 "\033%%1B"		// switch to HPGL
	 "SP1SC0,%.4f,0,-%.4f,2IR0,100,0,100" // set up scaling
	 "LA1,4,2,4"		// round line ends and joins
	 "PR"			// relative plotting
	 "TR0"			// opaque
	 ";\033%%1A"		// back to PCL
	 "\033&f1S",		// pop position
	 MAX_PAPER_WIDTH, MAX_PAPER_HEIGHT,
	 hpgl_scale, hpgl_scale);
}

void lj4_printer::set_line_thickness(int size, int dot)
{
  double pw;
  if (line_thickness < 0)
    pw = (size * (line_width_factor * 25.4))/(font::sizescale * 72000.0);
  else
    pw = line_thickness*25.4/font::res;
  if (dot && pw < MIN_DOT_PEN_WIDTH)
    pw = MIN_DOT_PEN_WIDTH;
  if (pw != pen_width) {
    printf("PW%f", pw);
    pen_width = pw;
  }
}

font *lj4_printer::make_font(const char *nm)
{
  return lj4_font::load_lj4_font(nm);
}

printer *make_printer()
{
  return new lj4_printer(user_paper_size);
}

static
int lookup_paper_size(const char *s)
{
  for (unsigned int i = 0;
       i < sizeof(paper_table)/sizeof(paper_table[0]); i++) {
    // FIXME Perhaps allow unique prefix.
    if (strcasecmp(s, paper_table[i].name) == 0)
      return i;
  }
  return -1;
}

static void usage(FILE *stream);

extern "C" int optopt, optind;

int main(int argc, char **argv)
{
  setlocale(LC_NUMERIC, "C");
  program_name = argv[0];
  static char stderr_buf[BUFSIZ];
  setbuf(stderr, stderr_buf);
  int c;
  static const struct option long_options[] = {
    { "help", no_argument, 0, CHAR_MAX + 1 },
    { "version", no_argument, 0, 'v' },
    { NULL, 0, 0, 0 }
  };
  while ((c = getopt_long(argc, argv, "c:d:F:I:lp:vw:", long_options, NULL))
	 != EOF)
    switch(c) {
    case 'l':
      landscape_flag = 1;
      break;
    case 'I':
      // ignore include search path
      break;
    case ':':
      if (optopt == 'd') {
	fprintf(stderr, "duplex assumed to be long-side\n");
	duplex_flag = 1;
      } else
	fprintf(stderr, "option -%c requires an argument\n", optopt);
      fflush(stderr);
      break;
    case 'd':
      if (!isdigit(*optarg))	// this ugly hack prevents -d without
	optind--;		//  args from messing up the arg list
      duplex_flag = atoi(optarg);
      if (duplex_flag != 1 && duplex_flag != 2) {
	fprintf(stderr, "odd value for duplex; assumed to be long-side\n");
	duplex_flag = 1;
      }
      break;
    case 'p':
      {
	int n = lookup_paper_size(optarg);
	if (n < 0)
	  error("ignoring invalid paper format '%1'", font::papersize);
	else
	  user_paper_size = n;
	break;
      }
    case 'v':
      printf("GNU grolj4 (groff) version %s\n", Version_string);
      exit(0);
      break;
    case 'F':
      font::command_line_font_dir(optarg);
      break;
    case 'c':
      {
	char *ptr;
	long n = strtol(optarg, &ptr, 10);
	if (n == 0 && ptr == optarg)
	  error("argument for -c must be a positive integer");
	else if (n <= 0 || n > 32767)
	  error("out of range argument for -c");
	else
	  ncopies = unsigned(n);
	break;
      }
    case 'w':
      {
	char *ptr;
	long n = strtol(optarg, &ptr, 10);
	if (n == 0 && ptr == optarg)
	  error("argument for -w must be a non-negative integer");
	else if (n < 0 || n > INT_MAX)
	  error("out of range argument for -w");
	else
	  line_width_factor = int(n);
	break;
      }
    case CHAR_MAX + 1: // --help
      usage(stdout);
      exit(0);
      break;
    case '?':
      usage(stderr);
      exit(1);
      break;
    default:
      assert(0);
    }
  SET_BINARY(fileno(stdout));
  if (optind >= argc)
    do_file("-");
  else {
    for (int i = optind; i < argc; i++)
      do_file(argv[i]);
  }
  return 0;
}

static void usage(FILE *stream)
{
  fprintf(stream,
	  "usage: %s [-l] [-c n] [-d [n]] [-F dir] [-p paper-format]"
	  " [-w n] [file ...]\n"
	  "usage: %s {-v | --version}\n"
	  "usage: %s --help\n",
	  program_name, program_name, program_name);
}

// Local Variables:
// fill-column: 72
// mode: C++
// End:
// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:
