/* pretty - MINIX prettyprinter		Author: Andy Tanenbaum */

/* This program is just a post processor for indent.  Unfortunately,
 * indent can't quite produce MINIX format output.  This program takes
 * the output from indent and fixes up a couple of items:
 *
 * 	- the first tab stop is at line_length 3 instead of 9
 *	- short 'if', 'while' and 'for' statements should be on a single line
 *	- placement of comments
 *	- return (value)  ==> return(value)
 *	- etc.
 *
 * To use pretty, install "indent" in /bin or /usr/bin.
 */

#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>

#define NAME_SIZE          14
#define PATH_MAX          255
#define MAX_LINE           78
#define BUF_SIZE         1024
#define TAB                 8
#define COM_COL            33

#define skip_whitespace(p)  while (*p == ' ' || *p == '\t') p++;

char buf[BUF_SIZE];
char buf2[BUF_SIZE];
char buf3[BUF_SIZE];
int loops;

_PROTOTYPE(int main, (int argc, char **argv));
_PROTOTYPE(void indent, (char *s, char *intermediate));
_PROTOTYPE(void process, (char *s, char *intermediate));
_PROTOTYPE(int join, (void));
_PROTOTYPE(void shift_left, (char *p));
_PROTOTYPE(void shift_right, (char *p));
_PROTOTYPE(int line_length, (char *p));
_PROTOTYPE(void place_comment, (char *buf));
_PROTOTYPE(void ifwhilefor, (FILE *in, FILE *out));
_PROTOTYPE(void do_return, (char *b));
_PROTOTYPE(void splice_comment, (FILE *in));
_PROTOTYPE(void add_blank, (void));
_PROTOTYPE(void usage, (void));
_PROTOTYPE(void do_case, (FILE *in));
_PROTOTYPE(void capitalize, (void));
_PROTOTYPE(void scrunch, (FILE *in));
_PROTOTYPE(void three_liner, (FILE *in));

int main(argc, argv)
int argc;
char *argv[];
{

  int i, n;
  char *name, intermediate[PATH_MAX+1], *p;

  if (argc < 2) usage();

  for (i = 1; i < argc; i++) {
	/* Invent a name for the intermediate file. */
	name = argv[i];
	p = name + strlen(name) - 1;
	while (p >= name && *p != '/') p--;
	p++;			/* p points to last component */
	strncpy(intermediate, p, (size_t)NAME_SIZE);
	if (strlen(intermediate) < NAME_SIZE) {
		/* Name is less than 14 chars.  Just add "+" to name. */
		strcat(intermediate, "+");
	} else {
		/* Name is 14 chars.  Invert case of last character. */
		n = strlen(intermediate);
		intermediate[n - 1] ^= 040;
	}

	indent(name, intermediate);
	process(name, intermediate);
	unlink(intermediate);
  }
  return(0);
}

void indent(s, intermediate)
char *s;			/* name of file to prettyprint */
char *intermediate;		/* name of intermediate file */
{
/* Call indent to indent the file.  Then rename it. */
  int n;
  char buf[BUF_SIZE];

  strcpy(buf, "indent ");
  strcat(buf, s);
  strcat(buf," -npro -bap -bbb -br -ncdb -cli0.5 -di1 -lp -npsl -nfc1 -nip\n");
  n = system(buf);
  if (n < 0) {
	fprintf(stderr, "pretty: cannot indent %s\n", s);
	exit(1);
  }

  /* Rename the intermediate file. */
  strcpy(buf, "mv ");
  strcat(buf, s);
  strcat(buf, " ");
  strcat(buf, intermediate);
  strcat(buf, "\n");
  n = system(buf);
  if (n < 0) {
	fprintf(stderr, "pretty: mv %s %s failed\n", s, intermediate);
	exit(1);
  }
}

void process(s, intermediate)
char *s;			/* name of file to prettyprint */
char *intermediate;		/* name of intermediate file */
{
/* File is now indented.  Post process it. */

  FILE *in, *out;
  char *p;

  in = fopen(intermediate, "r");	/* open intermediate file */
  if (in == NULL) {
	fprintf(stderr, "pretty: cannot open %s\n", intermediate);
	exit(1);
  }

  /* Create the output file. */
  out = fopen(s, "w");
  if (out == NULL) {
	fprintf(stderr, "pretty: cannot open %s for writing\n", s);
	exit(1);
  }

  /* Process the file a line at a time. */
  loops = 0;
  fgets(buf, BUF_SIZE, in);
  fgets(buf2, BUF_SIZE, in);
  while (1) {
	if (fgets(buf3, BUF_SIZE, in) == NULL) {
		if (buf[0] == '\0') {
			fclose(in);
			fclose(out);
			return;
		} else {
			buf3[0] = '\0';
		}
	}
	do_return(buf3);

	/* Lines beginning with \t ==> "  "; \t\t ==> \t */
	if (buf[0] == '\t') {
		/* Check for any subsequent tabs; add 1 if found. */
		p = &buf[0];
		if (buf[1] == '\t') {
			/* Line starts with two tabs. */
			shift_left(buf);

			/* If there are more tabs, compensate for lost tab. */
			skip_whitespace(p);
			while (*p != '\t' && *p != '\n') p++;
			if (*p == '\t') {
				shift_right(p);
				*p = '\t';	/* insert new tab */
			}
		} else {
			/* Line starts with one tab. */
			shift_right(buf);
			buf[0] = ' ';
			buf[1] = ' ';
		}
	}
	ifwhilefor(in, out);
	do_return(buf);
	do_case(in);
	place_comment(buf);	/* align comment right */
	splice_comment(in);
	three_liner(in);
	capitalize();
	add_blank();
	fputs(buf, out);
	strcpy(buf, buf2);
	strcpy(buf2, buf3);
	loops++;		/* keep track of loop iterations */
  }
}


int join()
{
  char *p, *q;
  int col;

  /* Don't join two if statements. */
  p = buf2;
  skip_whitespace(p);
  if (strncmp(p, "if ", (size_t)3) == 0) return(MAX_LINE + 100);

  /* Don't join if the third line is 'else'. */
  p = buf3;
  skip_whitespace(p);
  if (strncmp(p, "else", (size_t)4) == 0) return(MAX_LINE + 100);

  /* Determine how long the joined statements will be. */
  q = buf2;
  skip_whitespace(q);
  col = line_length(buf) + line_length(q) + 1;
  return(col);
}


void shift_left(p)
register char *p;
{
/* Copy a string to the left 1 line_length. */

  do {
	*p = *(p + 1);
	p++;
  } while (*p != 0);
}

void shift_right(p)
register char *p;
{
/* Copy a string to the right 1 line_length. */

  register char *q;

  q = p + strlen(p);		/* points to 0 byte */
  while (q >= p) {
	*(q + 1) = *q;		/* move character to the right */
	q--;
  }
}

int line_length(p)
char *p;
{
/* Determine the length of the line, handling tabs correctly. */

  int col = 1;

  while (*p != '\n' && *p != '\0') {
	if (*p != '\t') {
		col++;
	} else {
		col += TAB - ((col - 1) % TAB);
	}
	p++;
  }
  return(col - 1);
}

void place_comment(buf)
char *buf;
{
/* See if there is a comment that has to be readjusted to get it right. */

  int col, col2, comcol, last, quotes;
  char tmp[BUF_SIZE];
  char *p, *q, *rb, *rt;

  col = 1;
  p = buf;

  /* Skip any initial white space in buf. */
  while (*p == ' ' || *p == '\t') {
	if (*p == ' ')
		col++;
	else
		col += TAB - (col - 1) % TAB;
	p++;
  }
  if (*p == '\n') return;	/* empty line */
  if (*p == '/' && *(p + 1) == '*')
	return;			/* line starts with comment */

  /* Scan forward looking for a comment. */
  quotes = 0;
  while (1) {
	if (*p == '\n') return;
	if (*p == '"') quotes++;	/* careful about strings */
	if (*p == '/' && *(p + 1) == '*') break;
	if (*p != '\t')
		col++;
	else
		col += TAB - (col - 1) % TAB;
	if (*p != ' ' && *p != '\t') last = col;
	p++;
  }

  /* We found a comment.  Where should it go? */
  if (quotes & 1) return;	/* if this is inside a string, return */
  if (last <= COM_COL) {
	comcol = COM_COL;
  } else {
	comcol = ((last - 1) / TAB) * TAB + TAB + 1;
  }
  if (comcol == col) return;	/* it is ok as is */

  if (comcol > col) {
	/* Move comment to the right. */
	shift_right(p);
	*p = '\t';
	return;
  }

  /* Move comment to the left. */
  q = p - 1;			/* q points to char before comment */
  while (*q == ' ' || *q == '\t') q--;
  rb = buf;
  rt = tmp;
  col2 = 1;
  while (rb <= q) {
	if (*rb != '\t')
		col2++;
	else
		col2 += TAB - (col2 - 1) % TAB;
	*rt++ = *rb++;		/* copy one character */
  }
  while (col2 < comcol) {
	*rt++ = '\t';
	col2 += TAB - (col2 - 1) % TAB;
  }
  *rt = 0;
  strcat(rt, p);
  strcpy(buf, tmp);
}


void ifwhilefor(in, out)
FILE *in, *out;
{
/* Check for 'if' or 'while' split over two lines. */

  int is_if, is_while, is_for;
  char *first1, *first2, *second1;

  /* Check for 'if', 'while' or 'for' statements split over two lines. */
  first1 = &buf[0];
  skip_whitespace(first1);
  first2 = first1 + strlen(first1) - 2;	/* last char */
  while (first2 > &buf[0] && (*first2 == ' ' || *first2 == '\t')) first2--;

  /* First1 and first2 now point to first/last nonblank chars. */
  is_if = (strncmp(first1, "if ", (size_t)3) == 0 ? 1 : 0);
  is_for = (strncmp(first1, "for ", (size_t)4) == 0 ? 1 : 0);
  is_while = (strncmp(first1, "while ", (size_t)6) == 0 ? 1 : 0);

  if ((is_if || is_for || is_while) && *first2 == ')') {
	/* This is an 'if' or 'while' statement ending with ')'. */
	if (join() < MAX_LINE) {
		*(first2 + 1) = ' ';
		*(first2 + 2) = 0;
		second1 = &buf2[0];
		while (*second1 == ' ' || *second1 == '\t') second1++;
		strcat(first2, second1);
		scrunch(in);
	}
  }
}

void do_return(b)
char *b;
{
/* Remove the space after return, i.e., return (0) ==> return(0). */

  char *p;

  p = b;
  skip_whitespace(p);
  if (strncmp(p, "return (", (size_t)8) != 0) return;
  shift_left(p + 6);
}


void splice_comment(in)
FILE *in;
{
/* Indent has the problem that it sometimes breaks one line comments over two
 * lines, even though the original comment fits quite well on one line.  Fix.
 */

  char *p, *q;

  p = buf2;			/* affected lines are followed by * something */
  skip_whitespace(p);
  if (*p != '*' || *(p + 1) != ' ') return;

  /* Second line starts with * something.  See if first one contains
   * comment. */
  q = buf;
  while (1) {
	while (*q != '/' && *q != '\n') q++;
	if (*q == '\n') return;
	if (*(q + 1) == '*') break;
	q++;
  }

  /* We found a comment here.  See if the two lines can be merged. */
  if (line_length(buf) + line_length(p + 1) > MAX_LINE) return;
  q = buf + strlen(buf) - 1;	/* q points to '\n' */
  *q = 0;
  strcat(buf, p + 1);
  scrunch(in);
}


void add_blank()
{
/* Add a blank line between closing bracket and comment. */

  char *p, *q;
  p = buf;
  skip_whitespace(p);
  q = buf2;
  skip_whitespace(q);
  if (strcmp(p, "}\n") == 0 && strncmp(q, "/*", (size_t)2) == 0) {
	strcat(buf, "\n");
  }
}


void usage()
{
  fprintf(stderr, "Usage: pretty file ...\n");
}

void do_case(in)
FILE *in;
{
/* Try to get short cases all on 1 line. */

  char *p, *q, *r;

  p = buf;
  skip_whitespace(p);
  if (strncmp(p, "case ", (size_t)5) != 0 && strncmp(p, "default", (size_t)7) != 0) return;
  shift_right(p);
  *p++ = ' ';
  shift_right(p);
  *p++ = ' ';
  shift_right(p);
  *p++ = ' ';
  shift_right(p);
  *p++ = ' ';

  q = buf2;
  skip_whitespace(q);

  r = buf3;
  skip_whitespace(r);
  if (strncmp(r, "break;", (size_t)6) != 0 && strcmp(r, "}\n") != 0) return;

  /* This is a case statement and the case is only 1 line long (+ break). */
  if (line_length(buf) + 3 * TAB + line_length(q) + line_length(r) > MAX_LINE)
	return;
  p = buf + strlen(buf) - 1;
  *p = 0;
  strcat(buf, "\t");
  strcat(buf, q);
  p = buf + strlen(buf) - 1;
  *p = 0;
  if (strncmp(r, "break;", (size_t)6) == 0) {
	strcat(buf, "\t");	/* 'break' case */
  } else {
	strcat(buf, "\n");	/* 'default' case */
  }
  if (strncmp(r, "}", (size_t)1) == 0) {
	/* The "}' at the end of the switch has to be fudged. */
	p = buf;
	while (*p++ == '\t') strcat(buf, "\t");
  }
  strcat(buf, r);
  if (fgets(buf2, BUF_SIZE, in) == NULL) buf2[0] = '\0';
  if (fgets(buf3, BUF_SIZE, in) == NULL) buf3[0] = '\0';
}


void capitalize()
{
/* Capitalize the first word of comments that begin a section. */

  char *p, *q;

  if (loops == 0) return;
  p = buf;
  skip_whitespace(p);
  if (strncmp(p, "/* ", (size_t)3) != 0) return;
  q = p + 3;
  if (*q >= 'a' && *q <= 'z') *q = *q - 'a' + 'A';
}

void scrunch(in)
FILE *in;
{
/* Move buf3 to buf2 and reload buf3. */

  strcpy(buf2, buf3);
  if (fgets(buf3, BUF_SIZE, in) == NULL) buf3[0] = '\0';
}

void three_liner(in)
FILE *in;
{
/* Handle three line comments that get munged. */

  char *p, *q;

  p = buf;
  q = buf2;
  skip_whitespace(p);
  skip_whitespace(q);
  if (*p == '/' && *(p + 1) == '*' && *q == '*' && *(q + 1) == '/') {
	q = p + strlen(p) - 1;
	*q = 0;
	strcat(p, " */\n");
	scrunch(in);
  }
}
