// -*- C++ -*-
/* Copyright (C) 1989-2014  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/>. */

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

extern "C" const char *Version_string;

#define DEFAULT_LINEWIDTH 40
static int linewidth = DEFAULT_LINEWIDTH;

static int draw_flag = 1;

static int landscape_flag = 0;
static double user_paper_length = 0;
static double user_paper_width = 0;

/* These values were chosen because:

(MULTIPLIER*SIZESCALE)/(RES*UNITWIDTH) == 1/(2^20 * 72.27)

and 57816 is an exact multiple of both 72.27*SIZESCALE and 72.

The width in the groff font file is the product of MULTIPLIER and the
width in the tfm file. */

#define RES 57816
#define RES_7227 (RES/7227)
#define UNITWIDTH 131072
#define SIZESCALE 100
#define MULTIPLIER 1

class dvi_font : public font {
  dvi_font(const char *);
public:
  int checksum;
  int design_size;
  ~dvi_font();
  void handle_unknown_font_command(const char *command, const char *arg,
				   const char *filename, int lineno);
  static dvi_font *load_dvi_font(const char *);
};

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

dvi_font::dvi_font(const char *nm)
: font(nm), checksum(0), design_size(0)
{
}

dvi_font::~dvi_font()
{
}

void dvi_font::handle_unknown_font_command(const char *command,
					   const char *arg,
					   const char *filename, int lineno)
{
  char *ptr;
  if (strcmp(command, "checksum") == 0) {
    if (arg == 0)
      fatal_with_file_and_line(filename, lineno,
			       "`checksum' command requires an argument");
    checksum = int(strtol(arg, &ptr, 10));
    if (checksum == 0 && ptr == arg) {
      fatal_with_file_and_line(filename, lineno, "bad checksum");
    }
  }
  else if (strcmp(command, "designsize") == 0) {
    if (arg == 0)
      fatal_with_file_and_line(filename, lineno,
			       "`designsize' command requires an argument");
    design_size = int(strtol(arg, &ptr, 10));
    if (design_size == 0 && ptr == arg) {
      fatal_with_file_and_line(filename, lineno, "bad design size");
    }
  }
}

#define FONTS_MAX 256

struct output_font {
  dvi_font *f;
  int point_size;
  output_font() : f(0) { }
};

class dvi_printer : public printer {
  FILE *fp;
  int max_drift;
  int byte_count;
  int last_bop;
  int page_count;
  int cur_h;
  int cur_v;
  int end_h;
  int max_h;
  int max_v;
  output_font output_font_table[FONTS_MAX];
  font *cur_font;
  int cur_point_size;
  color cur_color;
  int pushed;
  int pushed_h;
  int pushed_v;
  int have_pushed;
  void preamble();
  void postamble();
  void define_font(int);
  void set_font(int);
  void possibly_begin_line();
  void set_color(color *);
protected:
  enum {
    id_byte = 2,
    set1 = 128,
    put1 = 133,
    put_rule = 137,
    bop = 139,
    eop = 140,
    push = 141,
    pop = 142,
    right1 = 143,
    down1 = 157,
    fnt_num_0 = 171,
    fnt1 = 235,
    xxx1 = 239,
    fnt_def1 = 243,
    pre = 247,
    post = 248,
    post_post = 249,
    filler = 223
  };
  int line_thickness;

  void out1(int);
  void out2(int);
  void out3(int);
  void out4(int);
  void moveto(int, int);
  void out_string(const char *);
  void out_signed(unsigned char, int);
  void out_unsigned(unsigned char, int);
  void do_special(const char *);
public:
  dvi_printer();
  ~dvi_printer();
  font *make_font(const char *);
  void begin_page(int);
  void end_page(int);
  void set_char(glyph *, font *, const environment *, int, const char *);
  void special(char *, const environment *, char);
  void end_of_line();
  void draw(int, int *, int, const environment *);
};


class draw_dvi_printer : public dvi_printer {
  int output_pen_size;
  void set_line_thickness(const environment *);
  void fill_next(const environment *);
public:
  draw_dvi_printer();
  ~draw_dvi_printer();
  void draw(int code, int *p, int np, const environment *env);
  void end_page(int);
};

dvi_printer::dvi_printer()
: fp(stdout), byte_count(0), last_bop(-1), page_count(0), max_h(0), max_v(0),
  cur_font(0), cur_point_size(-1), pushed(0), line_thickness(-1)
{
  if (font::res != RES)
    fatal("resolution must be %1", RES);
  if (font::unitwidth != UNITWIDTH)
    fatal("unitwidth must be %1", UNITWIDTH);
  if (font::hor != 1)
    fatal("hor must be equal to 1");
  if (font::vert != 1)
    fatal("vert must be equal to 1");
  if (font::sizescale != SIZESCALE)
    fatal("sizescale must be equal to %1", SIZESCALE);
  max_drift = font::res/1000;	// this is fairly arbitrary
  preamble();
}

dvi_printer::~dvi_printer()
{
  postamble();
}


draw_dvi_printer::draw_dvi_printer()
: output_pen_size(-1)
{
}

draw_dvi_printer::~draw_dvi_printer()
{
}


void dvi_printer::out1(int n)
{
  byte_count += 1;
  putc(n & 0xff, fp);
}

void dvi_printer::out2(int n)
{
  byte_count += 2;
  putc((n >> 8) & 0xff, fp);
  putc(n & 0xff, fp);
}

void dvi_printer::out3(int n)
{
  byte_count += 3;
  putc((n >> 16) & 0xff, fp);
  putc((n >> 8) & 0xff, fp);
  putc(n & 0xff, fp);
}

void dvi_printer::out4(int n)
{
  byte_count += 4;
  putc((n >> 24) & 0xff, fp);
  putc((n >> 16) & 0xff, fp);
  putc((n >> 8) & 0xff, fp);
  putc(n & 0xff, fp);
}

void dvi_printer::out_string(const char *s)
{
  out1(strlen(s));
  while (*s != 0)
    out1(*s++);
}


void dvi_printer::end_of_line()
{
  if (pushed) {
    out1(pop);
    pushed = 0;
    cur_h = pushed_h;
    cur_v = pushed_v;
  }
}

void dvi_printer::possibly_begin_line()
{
  if (!pushed) {
    have_pushed = pushed = 1;
    pushed_h = cur_h;
    pushed_v = cur_v;
    out1(push);
  }
}

int scale(int x, int z)
{
  int sw;
  int a, b, c, d;
  int alpha, beta;
  alpha = 16*z; beta = 16;
  while (z >= 040000000L) {
    z /= 2; beta /= 2;
  }
  d = x & 255;
  c = (x >> 8) & 255;
  b = (x >> 16) & 255;
  a = (x >> 24) & 255;
  sw = (((((d * z) / 0400) + (c * z)) / 0400) + (b * z)) / beta;
  if (a == 255)
    sw -= alpha;
  else
    assert(a == 0);
  return sw;
}

void dvi_printer::set_color(color *col)
{
  cur_color = *col;
  char buf[256];
  unsigned int components[4];
  color_scheme cs = col->get_components(components);
  switch (cs) {
  case DEFAULT:
    sprintf(buf, "color gray 0");
    break;
  case RGB:
    sprintf(buf, "color rgb %.3g %.3g %.3g",
		 double(Red) / color::MAX_COLOR_VAL,
		 double(Green) / color::MAX_COLOR_VAL,
		 double(Blue) / color::MAX_COLOR_VAL);
    break;
  case CMY:
    col->get_cmyk(&Cyan, &Magenta, &Yellow, &Black);
    // fall through
  case CMYK:
    sprintf(buf, "color cmyk %.3g %.3g %.3g %.3g",
		 double(Cyan) / color::MAX_COLOR_VAL,
		 double(Magenta) / color::MAX_COLOR_VAL,
		 double(Yellow) / color::MAX_COLOR_VAL,
		 double(Black) / color::MAX_COLOR_VAL);
    break;
  case GRAY:
    sprintf(buf, "color gray %.3g",
		 double(Gray) / color::MAX_COLOR_VAL);
    break;
  }
  do_special(buf);
}

void dvi_printer::set_char(glyph *g, font *f, const environment *env,
			   int w, const char *)
{
  if (*env->col != cur_color)
    set_color(env->col);
  int code = f->get_code(g);
  if (env->size != cur_point_size || f != cur_font) {
    cur_font = f;
    cur_point_size = env->size;
    int i;
    for (i = 0;; i++) {
      if (i >= FONTS_MAX) {
	fatal("too many output fonts required");
      }
      if (output_font_table[i].f == 0) {
	output_font_table[i].f = (dvi_font *)cur_font;
	output_font_table[i].point_size = cur_point_size;
	define_font(i);
      }
      if (output_font_table[i].f == cur_font
	  && output_font_table[i].point_size == cur_point_size)
	break;
    }
    set_font(i);
  }
  int distance = env->hpos - cur_h;
  if (env->hpos != end_h && distance != 0) {
    out_signed(right1, distance);
    cur_h = env->hpos;
  }
  else if (distance > max_drift) {
    out_signed(right1, distance - max_drift);
    cur_h = env->hpos - max_drift;
  }
  else if (distance < -max_drift) {
    out_signed(right1, distance + max_drift);
    cur_h = env->hpos + max_drift;
  }
  if (env->vpos != cur_v) {
    out_signed(down1, env->vpos - cur_v);
    cur_v = env->vpos;
  }
  possibly_begin_line();
  end_h = env->hpos + w;
  cur_h += scale(f->get_width(g, UNITWIDTH) / MULTIPLIER,
		 cur_point_size * RES_7227);
  if (cur_h > max_h)
    max_h = cur_h;
  if (cur_v > max_v)
    max_v = cur_v;
  if (code >= 0 && code <= 127)
    out1(code);
  else
    out_unsigned(set1, code);
}

void dvi_printer::define_font(int i)
{
  out_unsigned(fnt_def1, i);
  dvi_font *f = output_font_table[i].f;
  out4(f->checksum);
  out4(output_font_table[i].point_size*RES_7227);
  out4(int((double(f->design_size)/(1<<20))*RES_7227*100 + .5));
  const char *nm = f->get_internal_name();
  out1(0);
  out_string(nm);
}

void dvi_printer::set_font(int i)
{
  if (i >= 0 && i <= 63)
    out1(fnt_num_0 + i);
  else
    out_unsigned(fnt1, i);
}

void dvi_printer::out_signed(unsigned char base, int param)
{
  if (-128 <= param && param < 128) {
    out1(base);
    out1(param);
  }
  else if (-32768 <= param && param < 32768) {
    out1(base+1);
    out2(param);
  }
  else if (-(1 << 23) <= param && param < (1 << 23)) {
    out1(base+2);
    out3(param);
  }
  else {
    out1(base+3);
    out4(param);
  }
}

void dvi_printer::out_unsigned(unsigned char base, int param)
{
  if (param >= 0) {
    if (param < 256) {
      out1(base);
      out1(param);
    }
    else if (param < 65536) {
      out1(base+1);
      out2(param);
    }
    else if (param < (1 << 24)) {
      out1(base+2);
      out3(param);
    }
    else {
      out1(base+3);
      out4(param);
    }
  }
  else {
    out1(base+3);
    out4(param);
  }
}

void dvi_printer::preamble()
{
  out1(pre);
  out1(id_byte);
  out4(254000);
  out4(font::res);
  out4(1000);
  out1(0);
}

void dvi_printer::postamble()
{
  int tem = byte_count;
  out1(post);
  out4(last_bop);
  out4(254000);
  out4(font::res);
  out4(1000);
  out4(max_v);
  out4(max_h);
  out2(have_pushed); // stack depth
  out2(page_count);
  int i;
  for (i = 0; i < FONTS_MAX && output_font_table[i].f != 0; i++)
    define_font(i);
  out1(post_post);
  out4(tem);
  out1(id_byte);
  for (i = 0; i < 4 || byte_count % 4 != 0; i++)
    out1(filler);
}  
  
void dvi_printer::begin_page(int i)
{
  page_count++;
  int tem = byte_count;
  out1(bop);
  out4(i);
  for (int j = 1; j < 10; j++)
    out4(0);
  out4(last_bop);
  last_bop = tem;
  // By convention position (0,0) in a dvi file is placed at (1in, 1in).
  cur_h = font::res;
  cur_v = font::res;
  end_h = 0;
  if (page_count == 1) {
    char buf[256];
    // at least dvips uses this
    double length = user_paper_length ? user_paper_length :
					double(font::paperlength) / font::res;
    double width = user_paper_width ? user_paper_width :
				      double(font::paperwidth) / font::res;
    if (width > 0 && length > 0) {
      sprintf(buf, "papersize=%.3fin,%.3fin",
	      landscape_flag ? length : width,
	      landscape_flag ? width : length);
      do_special(buf);
    }
  }
  if (cur_color != default_color)
    set_color(&cur_color);
}

void dvi_printer::end_page(int)
{
  set_color(&default_color);
  if (pushed)
    end_of_line();
  out1(eop);
  cur_font = 0;
}

void draw_dvi_printer::end_page(int len)
{
  dvi_printer::end_page(len);
  output_pen_size = -1;
}

void dvi_printer::do_special(const char *s)
{
  int len = strlen(s);
  if (len == 0)
    return;
  possibly_begin_line();
  out_unsigned(xxx1, len);
  while (*s)
    out1(*s++);
}

void dvi_printer::special(char *arg, const environment *env, char type)
{
  if (type != 'p')
    return;
  moveto(env->hpos, env->vpos);
  do_special(arg);
}

void dvi_printer::moveto(int h, int v)
{
  if (h != cur_h) {
    out_signed(right1, h - cur_h);
    cur_h = h;
    if (cur_h > max_h)
      max_h = cur_h;
  }
  if (v != cur_v) {
    out_signed(down1, v - cur_v);
    cur_v = v;
    if (cur_v > max_v)
      max_v = cur_v;
  }
  end_h = 0;
}

void dvi_printer::draw(int code, int *p, int np, const environment *env)
{
  if (code == 'l') {
    int x = 0, y = 0;
    int height = 0, width = 0;
    int thickness;
    if (line_thickness < 0)
      thickness = env->size*RES_7227*linewidth/1000;
    else if (line_thickness > 0)
      thickness = line_thickness;
    else
      thickness = 1;
    if (np != 2) {
      error("2 arguments required for line");
    }
    else if (p[0] == 0) {
      // vertical rule
      if (p[1] > 0) {
	x = env->hpos - thickness/2;
	y = env->vpos + p[1] + thickness/2;
	height = p[1] + thickness;
	width = thickness;
      }
      else if (p[1] < 0) {
	x = env->hpos - thickness/2;
	y = env->vpos + thickness/2;
	height = thickness - p[1];
	width = thickness;
      }
    }
    else if (p[1] == 0) {
      if (p[0] > 0) {
	x = env->hpos - thickness/2;
	y = env->vpos + thickness/2;
	height = thickness;
	width = p[0] + thickness;
      }
      else if (p[0] < 0) {
	x = env->hpos - p[0] - thickness/2;
	y = env->vpos + thickness/2;
	height = thickness;
	width = thickness - p[0];
      }
    }
    if (height != 0) {
      moveto(x, y);
      out1(put_rule);
      out4(height);
      out4(width);
    }
  }
  else if (code == '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");
      else
	line_thickness = p[0];
    }
  }
  else if (code == 'R') {
    if (np != 2)
      error("2 arguments required for rule");
    else if (p[0] != 0 || p[1] != 0) {
      int dh = p[0];
      int dv = p[1];
      int oh = env->hpos;
      int ov = env->vpos;
      if (dv > 0) {
	ov += dv;
	dv = -dv;
      }
      if (dh < 0) {
	oh += dh;
	dh = -dh;
      }
      moveto(oh, ov);
      out1(put_rule);
      out4(-dv);
      out4(dh);
    }
  }
}

// XXX Will this overflow?

inline int milliinches(int n)
{
  return (n*1000 + font::res/2)/font::res;
}

void draw_dvi_printer::set_line_thickness(const environment *env)
{
  int desired_pen_size
    = milliinches(line_thickness < 0
		  // Will this overflow?
		  ? env->size*RES_7227*linewidth/1000
		  : line_thickness);
  if (desired_pen_size != output_pen_size) {
    char buf[256];
    sprintf(buf, "pn %d", desired_pen_size);
    do_special(buf);
    output_pen_size = desired_pen_size;
  }
}

void draw_dvi_printer::fill_next(const environment *env)
{
  unsigned int g;
  if (env->fill->is_default())
    g = 0;
  else {
    // currently, only BW support
    env->fill->get_gray(&g);
  }
  char buf[256];
  sprintf(buf, "sh %.3g", 1 - double(g)/color::MAX_COLOR_VAL);
  do_special(buf);
}

void draw_dvi_printer::draw(int code, int *p, int np, const environment *env)
{
  char buf[1024];
  int fill_flag = 0;
  switch (code) {
  case 'C':
    fill_flag = 1;
    // fall through
  case 'c':
    {
      // troff adds an extra argument to C
      if (np != 1 && !(code == 'C' && np == 2)) {
	error("1 argument required for circle");
	break;
      }
      moveto(env->hpos+p[0]/2, env->vpos);
      if (fill_flag)
	fill_next(env);
      else
	set_line_thickness(env);
      int rad;
      rad = milliinches(p[0]/2);
      sprintf(buf, "%s 0 0 %d %d 0 6.28319",
	      (fill_flag ? "ia" : "ar"),
	      rad,
	      rad);
      do_special(buf);
      break;
    }
  case 'l':
    if (np != 2) {
      error("2 arguments required for line");
      break;
    }
    moveto(env->hpos, env->vpos);
    set_line_thickness(env);
    do_special("pa 0 0");
    sprintf(buf, "pa %d %d", milliinches(p[0]), milliinches(p[1]));
    do_special(buf);
    do_special("fp");
    break;
  case 'E':
    fill_flag = 1;
    // fall through
  case 'e':
    if (np != 2) {
      error("2 arguments required for ellipse");
      break;
    }
    moveto(env->hpos+p[0]/2, env->vpos);
    if (fill_flag)
      fill_next(env);
    else
      set_line_thickness(env);
    sprintf(buf, "%s 0 0 %d %d 0 6.28319",
	    (fill_flag ? "ia" : "ar"),
	    milliinches(p[0]/2),
	    milliinches(p[1]/2));
    do_special(buf);
    break;
  case 'P':
    fill_flag = 1;
    // fall through
  case 'p':
    {
      if (np & 1) {
	error("even number of arguments required for polygon");
	break;
      }
      if (np == 0) {
	error("no arguments for polygon");
	break;
      }
      moveto(env->hpos, env->vpos);
      if (fill_flag)
	fill_next(env);
      else
	set_line_thickness(env);
      do_special("pa 0 0");
      int h = 0, v = 0;
      for (int i = 0; i < np; i += 2) {
	h += p[i];
	v += p[i+1];
	sprintf(buf, "pa %d %d", milliinches(h), milliinches(v));
	do_special(buf);
      }
      do_special("pa 0 0");
      do_special(fill_flag ? "ip" : "fp");
      break;
    }
  case '~':
    {
      if (np & 1) {
	error("even number of arguments required for spline");
	break;
      }
      if (np == 0) {
	error("no arguments for spline");
	break;
      }
      moveto(env->hpos, env->vpos);
      set_line_thickness(env);
      do_special("pa 0 0");
      int h = 0, v = 0;
      for (int i = 0; i < np; i += 2) {
	h += p[i];
	v += p[i+1];
	sprintf(buf, "pa %d %d", milliinches(h), milliinches(v));
	do_special(buf);
      }
      do_special("sp");
      break;
    }
  case 'a':
    {
      if (np != 4) {
	error("4 arguments required for arc");
	break;
      }
      set_line_thickness(env);
      double c[2];
      if (adjust_arc_center(p, c)) {
	int rad = milliinches(int(sqrt(c[0]*c[0] + c[1]*c[1]) + .5));
	moveto(env->hpos + int(c[0]), env->vpos + int(c[1]));
	double start = atan2(p[1] + p[3] - c[1], p[0] + p[2] - c[0]);
	double end = atan2(-c[1], -c[0]);
	if (end - start < 0)
	  start -= 2 * 3.14159265358;
	sprintf(buf, "ar 0 0 %d %d %f %f", rad, rad, start, end);
	do_special(buf);
      }
      else {
	moveto(env->hpos, env->vpos);
	do_special("pa 0 0");
	sprintf(buf,
		"pa %d %d",
		milliinches(p[0] + p[2]),
		milliinches(p[1] + p[3]));
	do_special(buf);
	do_special("fp");
      }
      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;
    }
  case 'R':
    {
      if (np != 2) {
	error("2 arguments required for rule");
	break;
      }
      int dh = p[0];
      if (dh == 0)
	break;
      int dv = p[1];
      if (dv == 0)
	break;
      int oh = env->hpos;
      int ov = env->vpos;
      if (dv > 0) {
	ov += dv;
	dv = -dv;
      }
      if (dh < 0) {
	oh += dh;
	dh = -dh;
      }
      moveto(oh, ov);
      out1(put_rule);
      out4(-dv);
      out4(dh);
      break;
    }
  default:
    error("unrecognised drawing command `%1'", char(code));
    break;
  }
}

font *dvi_printer::make_font(const char *nm)
{
  return dvi_font::load_dvi_font(nm);
}

printer *make_printer()
{
  if (draw_flag)
    return new draw_dvi_printer;
  else
    return new dvi_printer;
}

static void usage(FILE *stream);

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, "dF:I:lp:vw:", long_options, NULL))
	 != EOF)
    switch(c) {
    case 'd':
      draw_flag = 0;
      break;
    case 'l':
      landscape_flag = 1;
      break;
    case 'F':
      font::command_line_font_dir(optarg);
      break;
    case 'I':
      // ignore include search path
      break;
    case 'p':
      if (!font::scan_papersize(optarg, 0,
				&user_paper_length, &user_paper_width))
	error("invalid custom paper size `%1' ignored", optarg);
      break;
    case 'v':
      {
	printf("GNU grodvi (groff) version %s\n", Version_string);
	exit(0);
	break;
      }
    case 'w':
      if (sscanf(optarg, "%d", &linewidth) != 1
	  || linewidth < 0 || linewidth > 1000) {
	error("bad line width");
	linewidth = DEFAULT_LINEWIDTH;
      }
      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 [-dv] [-F dir] [-w n] [files ...]\n",
	  program_name);
}
