/*
  X11 window driver
*/

#include "wio.h"

#include "config.h"
#include "console.h"
#include "head.h"
#include "os.h"
#include "shared.h"
#include "status.h"

#include "X11/Intrinsic.h"
#include "X11/StringDefs.h"
#define XK_MISCELLANY /* We want ALL the keysyms */
#include "X11/keysymdef.h"

#define MAX_FONTS    4

#include "wio_util.c"

typedef struct
{
  Pixel fore;
  Pixel back;
  Pixel palette[8];
  Bool inverted;
  Bool justified;
  Font font[MAX_FONTS];
  char *wide;
  char *high;
  char *border;
} AppData, *AppDataPtr;

static AppData appdata;

static XtResource resources[] =
{
  {XtNforeground, XtCForeground, XtRPixel, sizeof(Pixel),
  XtOffset(AppDataPtr, fore), XtRString, "Black"},
  {XtNbackground, XtCBackground, XtRPixel, sizeof(Pixel),
  XtOffset(AppDataPtr, back), XtRString, "White"},

  {XtNjustify, XtCJustify, XtRBool, sizeof(Bool),
  XtOffset(AppDataPtr, justified), XtRString, "true"},
  {XtNreverseVideo, XtCReverseVideo, XtRBool, sizeof(Bool),
  XtOffset(AppDataPtr, inverted), XtRString, "false"},
  {"normal", "Normal", XtRFont, sizeof(Font),
  XtOffset(AppDataPtr, font[0]), XtRString, XtDefaultFont},
  {"bold", "Bold", XtRFont, sizeof(Font),
  XtOffset(AppDataPtr, font[1]), XtRString, XtDefaultFont},
  {"italic", "Italic", XtRFont, sizeof(Font),
  XtOffset(AppDataPtr, font[2]), XtRString, XtDefaultFont},
  {"mono", "Mono", XtRFont, sizeof(Font),
  XtOffset(AppDataPtr, font[3]), XtRString, XtDefaultFont},
  {"wide", "Wide", XtRString, sizeof(char *),
  XtOffset(AppDataPtr, wide), XtRString, "80"},
  {"high", "High", XtRString, sizeof(char *),
  XtOffset(AppDataPtr, high), XtRString, "32"},
  {"border", "Border", XtRString, sizeof(char *),
  XtOffset(AppDataPtr, border), XtRString, "8"}
};

typedef struct
{
  int wide, high, above, below;
  Font handle;
  XFontStruct *fs;
} font_data;

typedef struct
{
  int x, y;
} point;

typedef struct
{
  point min, max;
} box;

#define CLASSNAME   "Infocom"

static Widget toplevel, window_widget;
static GC gc;
static Pixmap pixmap;

static font_data font_inf[MAX_FONTS];
static point wind;
static int font_above, font_below, font_high;

static int border_size;

static int show_caret = 0, have_caret = 0;
static int have_seen_exposed = 0;

#define TEXT_SIZE       32

static int text_held = 0;
static char text_chars[TEXT_SIZE];

#define font_wide   (font_inf[3].wide)

#define x_display   XtDisplay(toplevel)
#define x_window    XtWindow(window_widget)

static bool colours_valid;
static Pixel pixel_colour[8];

static void alloc_colours(void)
{
  int i;
  int x_screen  = DefaultScreen(x_display);
  Colormap cmap = DefaultColormap(x_display, x_screen);
  colours_valid = DefaultDepth(x_display, x_screen) > 3;
  for(i = 0; i < 8 && colours_valid; ++i)
  {
    XColor color;
    color.red       = (i & 1) ? 65535 : 0;
    color.green     = (i & 2) ? 65535 : 0;
    color.blue      = (i & 4) ? 65535 : 0;
    colours_valid   = colours_valid && XAllocColor(x_display, cmap, &color);
    pixel_colour[i] = color.pixel;
  }
  if(!colours_valid)
    hd_no_colour();
}

static void event_poll(void)
{
  XEvent event;
  XtNextEvent(&event);
  XtDispatchEvent(&event);
}

static void load_fonts(void)
{
  int i;

  /* Most of the work has been done by the resource loader */
  for(i = 0; i < MAX_FONTS; ++i)
    font_inf[i].handle = appdata.font[i];
}

static void calc_fonts(void)
{
  int i;
  font_above = 0;
  font_below = 0;
  for(i = 0; i < MAX_FONTS; ++i)
  {
    XFontStruct *fs   = XQueryFont(x_display, font_inf[i].handle);
    font_inf[i].wide  = fs->max_bounds.width;
    font_inf[i].above = fs->max_bounds.ascent;
    font_inf[i].below = fs->max_bounds.descent;
    if(font_inf[i].above > font_above)
      font_above = font_inf[i].above;
    if(font_inf[i].below > font_below)
      font_below = font_inf[i].below;
    font_inf[i].fs = fs;
  }
  font_high = (font_above + font_below);
}

static int font_for(unsigned n)
{
  static int f[] =
  {
    0, /* FONT_NORMAL     */
    0, /* FONT_REVS       */
    1, /* FONT_BOLD       */
    2, /* FONT_EMPH       */
    3, /* FONT_FIXED      */
    3, /* FONT_FIXED_REVS */
  };
  return f[n-1];
}

static int width_text(char *t, int n, console_attr *attr)
{
  return XTextWidth(font_inf[font_for(attr->font)].fs, t, n);
}

#define pixel_for(c,d)     (colours_valid && 2 <= (c) && (c) <= 9 ? pixel_colour[(c)-2] : (d))

static int print_text(int x, char *t, int n, console_attr *attr)
{
  int w = width_text(t, n, attr);
  int r = is_reverse(attr->font);
  Pixel back = pixel_for(attr->back, appdata.back);
  Pixel fore = pixel_for(attr->fore, appdata.fore);
  if(r)
  {
    Pixel t = fore; fore = back; back = t;
  }
  XSetForeground(x_display, gc, back);
  XFillRectangle(x_display, pixmap, gc, x, 0, w, font_high);
  XSetForeground(x_display, gc, fore);
  XSetBackground(x_display, gc, back);
  XSetFont(x_display, gc, font_inf[font_for(attr->font)].handle);
  XDrawString(x_display, pixmap, gc, x, font_above, t, n);
  return x + w;
}

static int wipe_area(int x, int w, console_attr *attr)
{
  Pixel back = pixel_for(attr->back, appdata.back);
  XSetForeground(x_display, gc, back);
  XFillRectangle(x_display, pixmap, gc, x, 0, w, font_high);
  return x + w;
}

static int width_line(console_cell *slack, char *text, int held)
{
  int x, p, z = 0;
  for(x = p = 0; x < held; x = z)
  {
    z = after(slack, x, held);
    p += width_text(&text[x], z - x, &slack[x].attr);
  }
  return p;
}

static void repaint_fixed(int y)
{
  int i, p;
  for(i = p = 0; i < con.shape.fixed; ++i, p += font_wide)
  {
    char c = con.row[y].fixed[i].text;
    if(c)
      (void) print_text(p, &c, 1, &con.row[y].fixed[i].attr);
  }
}

static int repaint_just(int y, int held)
{
  int x, z = 0;
  int c = 0;
  int p = 0;
  int s = wind.x;
  char *text = text_of(y);
  for(x = 0; x < held; x = z)
  {
    z = after_space(con.row[y].slack, x, held);
    s -= width_text(&text[x], z - x, &con.row[y].slack[x].attr);
    while(z < held && is_a_space(con.row[y].slack[z]))
    {
      ++z;
      ++c;
    }
  }
  for(x = 0; x < held; x = z)
  {
    z = after_space(con.row[y].slack, x, held);
    p = print_text(p, &text[x], z - x, &con.row[y].slack[x].attr);
    while(z < held && is_a_space(con.row[y].slack[z]))
    {
      int d = s / c;
      p = wipe_area(p, d, &con.row[y].slack[z].attr);
      s -= d;
      c -= 1;
      ++z;
    }
  }
  return p;
}

static int repaint_left(int y, int held)
{
  int x, z = 0, p = 0;
  char *text = text_of(y);
  if(y == con.cursor.y && !con.cursor.align)
  {
    int c = width_line(con.row[y].slack, text, min(held, con.cursor.x));
    if(wind.x < c)
      p = wind.x - c;
  }
  for(x = 0; x < held; x = z)
  {
    z = after(con.row[y].slack, x, held);
    p = print_text(p, &text[x], z - x, &con.row[y].slack[x].attr);
  }
  return p;
}

static void repaint_slack(int y)
{
  int held   = find_eol(y);
  int p = held == 0 ? 0
        : con.row[y].just && appdata.justified ? repaint_just(y, held)
        : repaint_left(y, held);
  if(p < wind.x)
  {
    console_attr a;
    if(held)
      a = con.row[y].slack[held-1].attr;
    else
      a = con.cursor.attr;
    a.font = 1;
    wipe_area(p, wind.x - p, &a);
  }
}

static void repaint_cursor(void)
{
  int p;
  int wide = font_wide / 3;
  int high = font_high;
  if(con.cursor.align)
  {
    p = con.cursor.x * font_wide;
  }
  else
  {
    char *text = text_of(con.cursor.y);
    int c = width_line(con.row[con.cursor.y].slack, text, con.cursor.x);
    p = min(wind.x, c);
  }
  XSetForeground(x_display, gc, appdata.fore);
  if(have_caret)
  {
    XSetFunction(x_display, gc, GXinvert);
    XFillRectangle(x_display, pixmap, gc, p, 0, wide, high);
  }
  else
  {
    XDrawRectangle(x_display, pixmap, gc, p, 0, wide - 1, high - 1);
  }
  XSetFunction(x_display, gc, GXcopy);
}

void repaint(int y)
{
  if(have_seen_exposed && 0 <= y && y < con.shape.height)
  {
    repaint_slack(y);
    repaint_fixed(y);
    con.row[y].flag = 0;
    if(show_caret > 0 && y == con.cursor.y)
      repaint_cursor();
    XCopyArea(x_display, pixmap, x_window, gc, 0, 0, wind.x, 
      font_high, border_size, y * font_high + border_size);
    con.row[y].flag = 0;
  }
}

static void repaint_border(void)
{
  if(border_size)
  {
    int h = wind.y + 2 * border_size;
    int w = wind.x + 2 * border_size;
    XSetForeground(x_display, gc, appdata.back);
    XFillRectangle(x_display, x_window, gc, 0, 0, w, border_size);
    XFillRectangle(x_display, x_window, gc, 0,
      wind.y + border_size, w, border_size);
    XFillRectangle(x_display, x_window, gc, 0, 0, border_size, h);
    XFillRectangle(x_display, x_window, gc, wind.x + border_size,
      0, border_size, h);
  }
}

static void keys(Widget w, XEvent * e, String * s, Cardinal * n)
{
  char buff[TEXT_SIZE];
  KeySym keysym;
  XComposeStatus compose;
  char buff_has = XLookupString(&e->xkey, buff, sizeof(buff),
    &keysym, &compose);
  switch(keysym)
  {
    case XK_BackSpace:
      buff[0] = 'H' - '@';
      buff_has = 1;
      break;
    case XK_Left:
      buff[0] = 'B' - '@';
      buff_has = 1;
      break;
    case XK_Right:
      buff[0] = 'F' - '@';
      buff_has = 1;
      break;
    case XK_Down:
      buff[0] = 'N' - '@';
      buff_has = 1;
      break;
    case XK_Up:
      buff[0] = 'P' - '@';
      buff_has = 1;
      break;
    case XK_Begin:
      buff[0] = 'A' - '@';
      buff_has = 1;
      break;
    case XK_End:
      buff[0] = 'E' - '@';
      buff_has = 1;
      break;
    case XK_Delete:
      buff[0] = 'D' - '@';
      buff_has = 1;
      break;
  }
  if(buff_has && text_held + buff_has <= TEXT_SIZE)
  {
    os_mcpy(&text_chars[text_held], buff, buff_has * sizeof(buff[0]));
    text_held += buff_has;
  }
}

static void focus(Widget w, XtPointer data, XEvent * e, Boolean * proceed)
{
  static int have_focus = 0;
  /* Black magic: see O'Reilly, Vol 1, p313 */
  switch(e->type)
  {
    case FocusIn:
      have_focus = have_caret = 1;
      break;
    case FocusOut:
      have_focus = have_caret = 0;
      break;
    case EnterNotify:
      have_caret = e->xcrossing.focus;
      break;
    case LeaveNotify:
      have_caret = have_focus;
      break;
  }
  redraw_cursor();
  *proceed = TRUE;
}


static void redraw_all(void)
{
  int i = con.shape.height;
  repaint_border();
  while(--i >= 0)
    repaint(i);
}

static void redraw(Widget w, XtPointer data, XEvent * e, Boolean * proceed)
{
  int top = (e->xexpose.y - border_size) / font_high;
  int bot = (e->xexpose.y - border_size + e->xexpose.height + font_high - 1) / font_high;
  int i = bot;
  have_seen_exposed = 1;
  while(--i >= top)
    repaint(i);
  *proceed = TRUE;
}

/* Action handlers */

static void toggle_justify(Widget w, XEvent * e, String * s, Cardinal * n)
{
  int i = con.shape.height;
  appdata.justified = !appdata.justified;
  while(--i >= 0)
    if(con.row[i].just)
      repaint(i);
}

static void toggle_reverse(Widget w, XEvent * e, String * s, Cardinal * n)
{
  Pixel t = appdata.fore;
  appdata.fore = appdata.back;
  appdata.back = t;
  redraw_all();
}

static XtActionsRec actions[] =
{
  {"justify", toggle_justify},
  {"reverse", toggle_reverse},
  {"keys", keys}
};

/* Initialisation */

void pre_init_io(int *argc, char **argv)
{
  toplevel = XtInitialize(argv[0], CLASSNAME, (XrmOptionDescRec *) 0,
    0, argc, argv);
}

void init_io(void)
{
  Arg args[2];
  int screen_width, screen_height;

  XtGetApplicationResources(toplevel, &appdata, resources,
    XtNumber(resources), NULL, 0);

  if(appdata.inverted)
  {
    Pixel temp = appdata.fore;
    appdata.fore = appdata.back;
    appdata.back = temp;
  }

  screen_width = atoi(appdata.wide);
  if(screen_width <= 0)
    screen_width = 80;
  screen_height = atoi(appdata.high);
  if(screen_height <= 0)
    screen_height = 32;
  border_size = atoi(appdata.border);
  if(border_size < 0)
    border_size = 8;

  load_fonts();
  calc_fonts();

  wind.x = screen_width * font_wide;
  wind.y = font_high * screen_height;

  args[0].name  = XtNwidth;
  args[0].value = (Dimension) wind.x + 2 * border_size;
  args[1].name  = XtNheight;
  args[1].value = (Dimension) wind.y + 2 * border_size;
  window_widget = XtCreateManagedWidget("Infocom", widgetClass,
    toplevel, args, XtNumber(args));

  alloc_colours();

  init_console(screen_height, screen_width);

  gc = XCreateGC(x_display, DefaultRootWindow(x_display), 0L, NULL);

  XtAddEventHandler(window_widget, ExposureMask, FALSE,
     redraw, (XtPointer) 0);
  XtAddEventHandler(toplevel,
    EnterWindowMask | LeaveWindowMask | FocusChangeMask, FALSE,
    focus, (XtPointer) 0);

  XtAddActions(actions, XtNumber(actions));
  XtAugmentTranslations(window_widget, 
    XtParseTranslationTable("<Key>: keys()"));
  XtAugmentTranslations(toplevel,
    XtParseTranslationTable("<Key>: keys()"));

  XtSetMappedWhenManaged(toplevel, False);
  XtRealizeWidget(toplevel);
  {
    XSizeHints *sh = XAllocSizeHints();
    XWMHints *wmh = XAllocWMHints();
    if(sh != NULL)
    {
      sh->flags = PMaxSize | PMinSize;
      sh->min_width = sh->max_width = wind.x + 2 * border_size;
      sh->min_height = sh->max_height = wind.y + 2 * border_size;
    }
    if(wmh != NULL)
    {
      wmh->flags = StateHint | InputHint;
      wmh->initial_state = NormalState;
      wmh->input = True;
    }
    XSetWMProperties(x_display, XtWindow(toplevel), NULL,
      NULL, NULL, 0, sh, wmh, NULL);
  }
  XtMapWidget(toplevel);

  pixmap = XCreatePixmap(x_display, x_window, wind.x, font_high,
     DefaultDepthOfScreen(XtScreen(toplevel)));

  display("\nInfocom interpreter for X11\n" VERSION "\n\n");
}

void exit_io(void)
{
  display("\nInfocom has terminated - press any key to quit");
  get_ch();
}

static char ungot;

void unget_ch(char c)
{
  ungot = c;
}

char get_ch(void)
{
  char c;

  show_cursor();
  force_update();
  while(text_held == 0)
    event_poll();
  hide_cursor();
  c = text_chars[0];
  if(--text_held)
    os_mcpy(&text_chars[0], &text_chars[1], text_held);
  return c;
}

int char_width(char c)
{
  char buff[4];
  buff[0] = c;
  return XTextWidth(font_inf[font_for(con.cursor.attr.font)].fs, buff, 1);
}

int display_width(void)
{
  return wind.x;
}

void scrollup(void)
{
  int top = border_size + font_high * (con.top + 1);
  int bot = border_size + font_high * con.bottom;
  XCopyArea(x_display, x_window, x_window, gc, border_size,
    top, wind.x, bot - top, border_size, top - font_high);
}

void redraw_cursor(void)
{
  touch(con.cursor.y);
}

void hide_cursor(void)
{
  --show_caret;
  touch(con.cursor.y);
}

void show_cursor(void)
{
  ++show_caret;
  touch(con.cursor.y);
}

