#include <X11/Xlib.h>
#include <X11/Xutil.h>
#define XK_MISCELLANY
#include <X11/keysymdef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <locale.h>
#include <errno.h>
#include <libutil.h>

#define BW 2

#define FONT_US "-ncd-terminal-medium-r-normal--18-*-iso646-us"
#define FONT_SE "-ncd-terminal-medium-r-normal--18-*-iso646-se2"

Display *d;
Window w;
GC tgc, fgc;
Pixmap wsave, csave;
Atom wm_protocols, wm_delete_window;

int cols = 80;
int lines = 25;
int pheight = 66;

int wx = 0, wy = 0, ww, wh;
char *fg = "#acaaac";
char *bg = "black";

char *font_us = FONT_US;
char *font_se = FONT_SE;
XFontStruct *fi_us, *fi_se;
int cw, ch, asc, desc;
int x, y;

char *shell;

int ptyfd, xfd;

int glass = 0;

int baud, delay;


static void wrestore()
{
   XCopyArea(d, wsave, w, tgc, 0, 0, ww + (BW*2), wh + (BW*2), 0, 0);
}

static void cur(int state)
{
   static int first = 1;

   if (state) {
       XCopyArea(d, w, csave, tgc, x, y - asc, cw, ch, 0, 0);
       XFillRectangle(d, w, tgc, x, y - asc, cw, ch);
       XCopyArea(d, wsave, csave, tgc, x, y - asc, cw, ch, 0, 0);
       XFillRectangle(d, wsave, tgc, x, y - asc, cw, ch);
       first = 0;
   } else {
       if (! first) {
           XCopyArea(d, csave, w, tgc, 0, 0, cw, ch, x, y - asc);
           XCopyArea(d, csave, wsave, tgc, 0, 0, cw, ch, x, y - asc);
       }
   }
}

static void feed(void)
{
   XCopyArea(d, w, w, tgc, BW, BW + ch, ww, wh - ch, BW, BW);
   XFillRectangle(d, w, fgc, BW, BW + wh - ch, ww, ch);
   XCopyArea(d, wsave, wsave, tgc, BW, BW + ch, ww, wh - ch, BW, BW);
   XFillRectangle(d, wsave, fgc, BW, BW + wh - ch, ww, ch);
}

static int read_pty(void)
{
   static int line = 0;
   int i, n;
   unsigned char c;

#define print() if (glass) { \
   XDrawImageString(d, w, tgc, x, y, &c, 1); \
   XDrawImageString(d, wsave, tgc, x, y, &c, 1); \
} else { \
   XDrawString(d, w, tgc, x, y, &c, 1); \
   XDrawString(d, wsave, tgc, x, y, &c, 1); \
}

   if ((n = read(ptyfd, &c, 1)) <= 0)
       return n;

   if (delay)
       usleep(delay);

   if (glass)
       cur(0);

   switch (c) {
       case 8:
           if (x > BW)
               x -= cw;
           break;
       case 10:
           feed();
           if (++line == pheight)
               line = 0;
           break;
       case 12:
           for (i = 0; i < (pheight - line); i++)
               feed();
           line = 0;
           break;
       case 13:
           x = BW;
           break;
       default:
           if ((c < 32) || ((c > 126) && (c < 160)))
               break;
           if (x == BW + ww) {
               feed();
               x = BW;
           }
           print();
           x += cw;
   }

   if (glass)
       cur(1);

   return n;
}

static void write_pty(XEvent *e)
{
   KeySym ks;
   unsigned char c;

   XLookupString((XKeyEvent *)e, &c, 1, &ks, NULL);
   switch (ks) {
       case XK_F9:
           XSetFont(d, tgc, fi_us->fid);
           return;
       case XK_F10:
           XSetFont(d, tgc, fi_se->fid);
           return;
   }
   if (c != 0) {
       if (delay)
           usleep(delay);
       write(ptyfd, &c, 1);
   }
}

static void usage(char *name)
{
   fprintf(stderr, "usage: %s [-x xpos] [-y ypos] [-c columns] [-l lines] [-f us_font] [-s se_font] [-b baud]\n", name);
   exit(1);
}

static void init(int argc, char **argv)
{
   XColor fgcol, bgcol;
   XClassHint *chint;
   XSizeHints *shints;
   char *name;
   int pid;
   int c, mod;

   if ((name = strrchr(argv[0], '/')))
       name++;
   else
       name = argv[0];
   if (! strcmp(name, "ttyemu")) {
       fprintf(stderr, "Error: wrong invocation name\n");
       exit(1);
   }
   glass = ! strcmp(name, "glasstty");

   baud = glass ? 300 : 110;

   while ((c = getopt(argc, argv, ":x:y:c:l:f:s:b:")) != -1) {
       switch (c) {
           case 'x':
               wx = atoi(optarg);
               break;
           case 'y':
               wy = atoi(optarg);
               break;
           case 'c':
               cols = atoi(optarg);
               break;
           case 'l':
               lines = atoi(optarg);
               break;
           case 'f':
               font_us = (char *)malloc(strlen(optarg) + 1);
               strcpy(font_us, optarg);
               break;
           case 's':
               font_se = (char *)malloc(strlen(optarg) + 1);
               strcpy(font_se, optarg);
               break;
           case 'b':
               baud = atoi(optarg);
               break;
           case ':':
           case '?':
           default:
               usage(name);
       }
   }

   if (! (pid = forkpty(&ptyfd, NULL, NULL, NULL))) {
       char *shell, *tail, shminus[80];
       setenv(glass ? "GLASSTTY" : "HCTTY", "1", 1);
       if (! (shell = getenv("SHELL")))
           shell = "/bin/sh";
       tail = strrchr(shell, '/') + 1;
       sprintf(shminus, "-%s", tail);
       execl(shell, shminus, (char *)NULL);
       perror("execl");
       _exit(1);
   } else if (pid < 0) {
       perror("fork");
       exit(1);
   }

   setlocale(LC_ALL, "");

   if (! (d = XOpenDisplay(NULL))) {
       fprintf(stderr, "Error opening display!\n");
       exit(1);
   }

   xfd = ConnectionNumber(d);

   if (! (fi_us = XLoadQueryFont(d, font_us))) {
       fprintf(stderr, "Error loading font!\n");
       exit(1);
   }

   if (! (fi_se = XLoadQueryFont(d, font_se))) {
       fprintf(stderr, "Error loading font!\n");
       exit(1);
   }

   cw = fi_us->per_char[32].width;
   ch = fi_us->ascent + fi_us->descent;

   XAllocNamedColor(d, DefaultColormap(d, 0), fg, &fgcol, &fgcol);
   XAllocNamedColor(d, DefaultColormap(d, 0), bg, &bgcol, &bgcol);

   ww = cw * cols;
   wh = ch * lines;

   if (! (w = XCreateSimpleWindow(d, DefaultRootWindow(d), wx, wy,
           ww + (BW*2), wh + (BW*2), 0, bgcol.pixel, bgcol.pixel)) ) {
       printf("Error creating window!\n");
       exit(1);
   }

   XSelectInput(d, w, KeyPressMask|ExposureMask);

   chint = XAllocClassHint();
   chint->res_name = (glass ? "glasstty" : "hctty");
   chint->res_class = (glass ? "Glasstty" : "Hctty");
   XSetClassHint(d, w, chint);
   XStoreName(d, w, glass ? "Glass TTY" : "Hardcopy TTY");

   shints = XAllocSizeHints();
   shints->x = wx;
   shints->y = wy;
   shints->flags = USPosition;
   XSetWMNormalHints(d, w, shints);

   wm_protocols = XInternAtom(d, "WM_PROTOCOLS", False);
   wm_delete_window = XInternAtom(d, "WM_DELETE_WINDOW", False);
   XSetWMProtocols(d, w, &wm_delete_window, 1);

   tgc = XCreateGC(d, w, 0, NULL);
   XSetForeground(d, tgc, fgcol.pixel);
   XSetBackground(d, tgc, bgcol.pixel);
   XSetFont(d, tgc, fi_us->fid);

   fgc = XCreateGC(d, w, 0, NULL);
   XSetForeground(d, fgc, bgcol.pixel);

   wsave = XCreatePixmap(d, w, ww + (BW*2), wh + (BW*2), DefaultDepth(d, 0));
   XFillRectangle(d, wsave, fgc, 0, 0, ww + (BW*2), wh + (BW*2));

   if (glass)
       csave = XCreatePixmap(d, w, cw, ch, DefaultDepth(d, 0));

   x = BW;
   y = BW + wh - fi_us->descent;
   asc = fi_us->ascent;
   desc = fi_us->descent;

   delay = 1000000 / ((double)baud / (glass ? 10. : 11.));
   mod = delay % 10000;
   delay = (mod > 5000 ? delay + (10000 - mod) : delay - mod) - 10000;
   if (delay < 0)
       delay = 0;

   XMapWindow(d, w);
}

static void quit()
{
   XDestroyWindow(d, w);
   XCloseDisplay(d);
   exit(0);
}

int main(int argc, char **argv)
{
   init(argc, argv);

   for (;;) {
       XEvent e;
       fd_set io_set;
       struct timeval timeout;

       while (XPending(d)) {
           XNextEvent(d, &e);
           switch (e.type) {
               case Expose:
                   if (! e.xexpose.count)
                       wrestore();
                   break;
               case KeyPress:
                   write_pty(&e);
                   break;
               case ClientMessage:
                   if (e.xclient.message_type == wm_protocols &&
                           e.xclient.data.l[0] == wm_delete_window)
                       quit();
                   break;
           }
       }

       FD_ZERO(&io_set);
       FD_SET(ptyfd, &io_set);
       FD_SET(xfd, &io_set);
       timeout.tv_sec = 0;
       timeout.tv_usec = 5000;

       if (select(xfd + 1, &io_set, NULL, NULL, &timeout) <= 0)
           continue;

       if (FD_ISSET(ptyfd, &io_set)) {
           if (read_pty() <= 0)
               quit();
       }
   }
}