/*************************************************************************
 * Copyright (c) 2011 AT&T Intellectual Property
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html
 *
 * Contributors: Details at https://graphviz.org
 *************************************************************************/

#include "config.h"

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#include <sys/types.h>
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#ifdef HAVE_SYS_INOTIFY_H
#include <sys/inotify.h>
#ifdef __sun
#include <sys/filio.h>
#endif
#endif
#include <errno.h>

#include <gvc/gvplugin_device.h>
#include <util/agxbuf.h>
#include <util/exit.h>
#include <util/gv_math.h>
#include <util/prisize_t.h>

#include <cairo.h>
#ifdef CAIRO_HAS_XLIB_SURFACE
#include <X11/Xutil.h>
#include <X11/extensions/Xrender.h>
#include <cairo-xlib.h>

typedef struct window_xlib_s {
  Window win;
  uint64_t event_mask;
  Pixmap pix;
  GC gc;
  Visual *visual;
  Colormap cmap;
  int depth;
  Atom wm_delete_window_atom;
} window_t;

static void handle_configure_notify(GVJ_t *job, XConfigureEvent *cev) {
  /*FIXME - should allow for margins */
  /*	- similar zoom_to_fit code exists in: */
  /*	plugin/gtk/callbacks.c */
  /*	plugin/xlib/gvdevice_xlib.c */
  /*	lib/gvc/gvevent.c */

  assert(cev->width >= 0 && "Xlib returned an event with negative width");
  assert(cev->height >= 0 && "Xlib returned an event with negative height");

  job->zoom *= 1 + fmin(((double)cev->width - job->width) / job->width,
                        ((double)cev->height - job->height) / job->height);
  if ((unsigned)cev->width > job->width || (unsigned)cev->height > job->height)
    job->has_grown = true;
  job->width = (unsigned)cev->width;
  job->height = (unsigned)cev->height;
  job->needs_refresh = true;
}

static void handle_expose(GVJ_t *job, XExposeEvent *eev) {
  window_t *window;

  window = job->window;
  assert(eev->width >= 0 &&
         "Xlib returned an expose event with negative width");
  assert(eev->height >= 0 &&
         "Xlib returned an expose event with negative height");
  XCopyArea(eev->display, window->pix, eev->window, window->gc, eev->x, eev->y,
            (unsigned)eev->width, (unsigned)eev->height, eev->x, eev->y);
}

static void handle_client_message(GVJ_t *job, XClientMessageEvent *cmev) {
  window_t *window;

  window = job->window;
  if (cmev->format == 32 &&
      (Atom)cmev->data.l[0] == window->wm_delete_window_atom)
    graphviz_exit(0);
}

static bool handle_keypress(GVJ_t *job, XKeyEvent *kev) {
  const KeyCode *const keycodes = job->keycodes;
  for (size_t i = 0; i < job->numkeys; i++) {
    if (kev->keycode == keycodes[i])
      return job->keybindings[i].callback(job) != 0;
  }
  return false;
}

static void browser_show(GVJ_t *job) {
  char *exec_argv[3] = {BROWSER, NULL, NULL};
  pid_t pid;

  exec_argv[1] = job->selected_href;

  pid = fork();
  if (pid == -1) {
    fprintf(stderr, "fork failed: %s\n", strerror(errno));
  } else if (pid == 0) {
    execvp(exec_argv[0], exec_argv);
    fprintf(stderr, "error starting %s: %s\n", exec_argv[0], strerror(errno));
  }
}

static int handle_xlib_events(GVJ_t *firstjob, Display *dpy) {
  GVJ_t *job;
  window_t *window;
  XEvent xev;
  pointf pointer;
  int rc = 0;

  while (XPending(dpy)) {
    XNextEvent(dpy, &xev);

    for (job = firstjob; job; job = job->next_active) {
      window = job->window;
      if (xev.xany.window == window->win) {
        switch (xev.xany.type) {
        case ButtonPress:
          pointer.x = xev.xbutton.x;
          pointer.y = xev.xbutton.y;
          assert(xev.xbutton.button <= INT_MAX &&
                 "Xlib returned invalid button event");
          job->callbacks->button_press(job, (int)xev.xbutton.button, pointer);
          rc++;
          break;
        case MotionNotify:
          if (job->button) { /* only interested while a button is pressed */
            pointer.x = xev.xbutton.x;
            pointer.y = xev.xbutton.y;
            job->callbacks->motion(job, pointer);
            rc++;
          }
          break;
        case ButtonRelease:
          pointer.x = xev.xbutton.x;
          pointer.y = xev.xbutton.y;
          assert(xev.xbutton.button <= INT_MAX &&
                 "Xlib returned invalid button event");
          job->callbacks->button_release(job, (int)xev.xbutton.button, pointer);
          if (job->selected_href && job->selected_href[0] &&
              xev.xbutton.button == 1)
            browser_show(job);
          rc++;
          break;
        case KeyPress:
          if (handle_keypress(job, &xev.xkey))
            return -1; /* exit code */
          rc++;
          break;
        case ConfigureNotify:
          handle_configure_notify(job, &xev.xconfigure);
          rc++;
          break;
        case Expose:
          handle_expose(job, &xev.xexpose);
          rc++;
          break;
        case ClientMessage:
          handle_client_message(job, &xev.xclient);
          rc++;
          break;
        default:
          break;
        }
        break;
      }
    }
  }
  return rc;
}

static void update_display(GVJ_t *job, Display *dpy) {
  window_t *window;
  cairo_surface_t *surface;

  window = job->window;

  // window geometry is set to fixed values
  assert(job->width <= INT_MAX && "out of range width");
  assert(job->height <= INT_MAX && "out of range height");

  if (job->has_grown) {
    XFreePixmap(dpy, window->pix);
    assert(window->depth >= 0 && "Xlib returned invalid window depth");
    window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
                                (unsigned)window->depth);
    job->has_grown = false;
    job->needs_refresh = true;
  }
  if (job->needs_refresh) {
    XFillRectangle(dpy, window->pix, window->gc, 0, 0, job->width, job->height);
    surface = cairo_xlib_surface_create(dpy, window->pix, window->visual,
                                        (int)job->width, (int)job->height);
    job->context = cairo_create(surface);
    job->external_context = true;
    job->callbacks->refresh(job);
    cairo_surface_destroy(surface);
    XCopyArea(dpy, window->pix, window->win, window->gc, 0, 0, job->width,
              job->height, 0, 0);
    job->needs_refresh = false;
  }
}

static void init_window(GVJ_t *job, Display *dpy, int scr) {
  const char *base = "";
  XGCValues gcv;
  XSetWindowAttributes attributes;
  XWMHints *wmhints;
  XSizeHints *normalhints;
  XClassHint *classhint;
  uint64_t attributemask = 0;
  window_t *window;
  double zoom_to_fit;

  window = malloc(sizeof(window_t));
  if (window == NULL) {
    fprintf(stderr, "Failed to malloc window_t\n");
    return;
  }

  unsigned w =
      480; /* FIXME - w,h should be set by a --geometry commandline option */
  unsigned h = 325;

  zoom_to_fit = fmin((double)w / job->width, (double)h / job->height);
  if (zoom_to_fit < 1.0) /* don't make bigger */
    job->zoom *= zoom_to_fit;

  job->width = w; /* use window geometry */
  job->height = h;

  job->window = window;
  job->fit_mode = false;
  job->needs_refresh = true;

  window->cmap = DefaultColormap(dpy, scr);
  window->visual = DefaultVisual(dpy, scr);
  attributes.background_pixel = WhitePixel(dpy, scr);
  attributes.border_pixel = BlackPixel(dpy, scr);
  attributemask = (CWBackPixel | CWBorderPixel);
  window->depth = DefaultDepth(dpy, scr);

  window->win = XCreateWindow(dpy, RootWindow(dpy, scr), 0, 0, job->width,
                              job->height, 0, window->depth, InputOutput,
                              window->visual, attributemask, &attributes);

  agxbuf name = {0};
  agxbprint(&name, "graphviz: %s", base);

  normalhints = XAllocSizeHints();
  normalhints->flags = 0;
  normalhints->x = 0;
  normalhints->y = 0;
  assert(job->width <= INT_MAX && "out of range width");
  normalhints->width = (int)job->width;
  assert(job->height <= INT_MAX && "out of range height");
  normalhints->height = (int)job->height;

  classhint = XAllocClassHint();
  classhint->res_name = "graphviz";
  classhint->res_class = "Graphviz";

  wmhints = XAllocWMHints();
  wmhints->flags = InputHint;
  wmhints->input = True;

  Xutf8SetWMProperties(dpy, window->win, agxbuse(&name), base, 0, 0,
                       normalhints, wmhints, classhint);
  XFree(wmhints);
  XFree(classhint);
  XFree(normalhints);
  agxbfree(&name);

  assert(window->depth >= 0 && "Xlib returned invalid window depth");
  window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
                              (unsigned)window->depth);
  gcv.foreground = WhitePixel(dpy, scr);
  window->gc = XCreateGC(dpy, window->pix, GCForeground, &gcv);
  update_display(job, dpy);

  window->event_mask =
      (ButtonPressMask | ButtonReleaseMask | PointerMotionMask | KeyPressMask |
       StructureNotifyMask | ExposureMask);
  XSelectInput(dpy, window->win, (long)window->event_mask);
  window->wm_delete_window_atom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
  XSetWMProtocols(dpy, window->win, &window->wm_delete_window_atom, 1);
  XMapWindow(dpy, window->win);
}

static int handle_stdin_events(GVJ_t *job) {

  if (feof(stdin))
    return -1;
  job->callbacks->read(job, job->input_filename, job->layout_type);

  return 1;
}

#ifdef HAVE_SYS_INOTIFY_H
static int handle_file_events(GVJ_t *job, int inotify_fd) {
  int avail, ret, len, rc = 0;
  char *bf, *p;
  struct inotify_event *event;

  ret = ioctl(inotify_fd, FIONREAD, &avail);
  if (ret < 0) {
    fprintf(stderr, "ioctl() failed\n");
    return -1;
  }

  if (avail) {
    assert(avail > 0 && "invalid value from FIONREAD");
    void *buf = malloc((size_t)avail);
    if (!buf) {
      fprintf(stderr, "out of memory (could not allocate %d bytes)\n", avail);
      return -1;
    }
    len = (int)read(inotify_fd, buf, (size_t)avail);
    if (len != avail) {
      fprintf(stderr, "avail = %d, len = %d\n", avail, len);
      free(buf);
      return -1;
    }
    bf = buf;
    while (len > 0) {
      event = (struct inotify_event *)bf;
      if (event->mask == IN_MODIFY) {
        p = strrchr(job->input_filename, '/');
        if (p)
          p++;
        else
          p = job->input_filename;
        if (strcmp(event->name, p) == 0) {
          job->callbacks->read(job, job->input_filename, job->layout_type);
          rc++;
        }
      }
      size_t ln = event->len + sizeof(struct inotify_event);
      assert(ln <= (size_t)len);
      bf += ln;
      len -= (int)ln;
    }
    free(buf);
    if (len != 0) {
      fprintf(stderr, "length miscalculation, len = %d\n", len);
      return -1;
    }
  }
  return rc;
}
#endif

static void xlib_initialize(GVJ_t *firstjob) {
  KeySym keysym;
  KeyCode *keycodes;
  const char *display_name = NULL;
  int scr;

  Display *dpy = XOpenDisplay(display_name);
  if (dpy == NULL) {
    fprintf(stderr, "Failed to open XLIB display: %s\n", XDisplayName(NULL));
    return;
  }
  scr = DefaultScreen(dpy);

  firstjob->screen = scr;

  keycodes = malloc(firstjob->numkeys * sizeof(KeyCode));
  if (firstjob->numkeys > 0 && keycodes == NULL) {
    fprintf(stderr, "Failed to malloc %" PRISIZE_T "*KeyCode\n",
            firstjob->numkeys);
    XCloseDisplay(dpy);
    return;
  }
  for (size_t i = 0; i < firstjob->numkeys; i++) {
    keysym = XStringToKeysym(firstjob->keybindings[i].keystring);
    if (keysym == NoSymbol)
      fprintf(stderr, "ERROR: No keysym for \"%s\"\n",
              firstjob->keybindings[i].keystring);
    else
      keycodes[i] = XKeysymToKeycode(dpy, keysym);
  }
  firstjob->keycodes = keycodes;

  firstjob->device_dpi.x =
      DisplayWidth(dpy, scr) * 25.4 / DisplayWidthMM(dpy, scr);
  firstjob->device_dpi.y =
      DisplayHeight(dpy, scr) * 25.4 / DisplayHeightMM(dpy, scr);
  firstjob->device_sets_dpi = true;

  firstjob->display = dpy;
}

static void xlib_finalize(GVJ_t *firstjob) {
  GVJ_t *job;
  Display *dpy = firstjob->display;
  int scr = firstjob->screen;
  KeyCode *keycodes = firstjob->keycodes;
  int numfds, stdin_fd = 0, xlib_fd, ret, events;
  fd_set rfds;
  bool watching_stdin_p = false;
#ifdef HAVE_SYS_INOTIFY_H
  int wd = 0;
  int inotify_fd = 0;
  bool watching_file_p = false;
  char *p, *cwd = NULL;

#ifdef HAVE_INOTIFY_INIT1
#ifdef IN_CLOSEXEC
  inotify_fd = inotify_init1(IN_CLOSEXEC);
#else
  inotify_fd = inotify_init1(IN_CLOEXEC);
#endif
#else
  inotify_fd = inotify_init();
  if (inotify_fd >= 0) {
    const int flags = fcntl(inotify_fd, F_GETFD);
    if (fcntl(inotify_fd, F_SETFD, flags | FD_CLOEXEC) < 0) {
      fprintf(stderr, "setting FD_CLOEXEC failed\n");
      return;
    }
  }
#endif
  if (inotify_fd < 0) {
    fprintf(stderr, "inotify_init() failed\n");
    return;
  }
#endif

  /* skip if initialization previously failed */
  if (dpy == NULL) {
    return;
  }

  numfds = xlib_fd = XConnectionNumber(dpy);

  if (firstjob->input_filename) {
    if (firstjob->graph_index == 0) {
#ifdef HAVE_SYS_INOTIFY_H
      watching_file_p = true;

      agxbuf dir = {0};
      if (firstjob->input_filename[0] != '/') {
        cwd = getcwd(NULL, 0);
        agxbprint(&dir, "%s/%s", cwd, firstjob->input_filename);
        free(cwd);
      } else {
        agxbput(&dir, firstjob->input_filename);
      }
      char *dirstr = agxbuse(&dir);
      p = strrchr(dirstr, '/');
      *p = '\0';

      wd = inotify_add_watch(inotify_fd, dirstr, IN_MODIFY);
      agxbfree(&dir);

      numfds = imax(inotify_fd, numfds);
#endif
    }
  } else {
    watching_stdin_p = true;
#ifdef F_DUPFD_CLOEXEC
    stdin_fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0);
#else
    stdin_fd = fcntl(STDIN_FILENO, F_DUPFD, 0);
    (void)fcntl(stdin_fd, F_SETFD, fcntl(stdin_fd, F_GETFD) | FD_CLOEXEC);
#endif
    numfds = imax(stdin_fd, numfds);
  }

  for (job = firstjob; job; job = job->next_active)
    init_window(job, dpy, scr);

  /* This is the event loop */
  FD_ZERO(&rfds);
  while (1) {
    events = 0;

#ifdef HAVE_SYS_INOTIFY_H
    if (watching_file_p) {
      if (FD_ISSET(inotify_fd, &rfds)) {
        ret = handle_file_events(firstjob, inotify_fd);
        if (ret < 0)
          break;
        events += ret;
      }
      FD_SET(inotify_fd, &rfds);
    }
#endif

    if (watching_stdin_p) {
      if (FD_ISSET(stdin_fd, &rfds)) {
        ret = handle_stdin_events(firstjob);
        if (ret < 0) {
          watching_stdin_p = false;
          FD_CLR(stdin_fd, &rfds);
        }
        events += ret;
      }
      if (watching_stdin_p)
        FD_SET(stdin_fd, &rfds);
    }

    ret = handle_xlib_events(firstjob, dpy);
    if (ret < 0)
      break;
    events += ret;
    FD_SET(xlib_fd, &rfds);

    if (events) {
      for (job = firstjob; job; job = job->next_active)
        update_display(job, dpy);
      XFlush(dpy);
    }

    ret = select(numfds + 1, &rfds, NULL, NULL, NULL);
    if (ret < 0) {
      fprintf(stderr, "select() failed\n");
      break;
    }
  }

#ifdef HAVE_SYS_INOTIFY_H
  if (watching_file_p)
    (void)inotify_rm_watch(inotify_fd, wd);
#endif

  XCloseDisplay(dpy);
  firstjob->display = NULL;
  free(keycodes);
  firstjob->keycodes = NULL;
}

static gvdevice_features_t device_features_xlib = {
    GVDEVICE_DOES_TRUECOLOR | GVDEVICE_EVENTS, /* flags */
    {0., 0.},                                  /* default margin - points */
    {0., 0.},   /* default page width, height - points */
    {96., 96.}, /* dpi */
};

static gvdevice_engine_t device_engine_xlib = {
    xlib_initialize,
    NULL, /* xlib_format */
    xlib_finalize,
};
#endif

gvplugin_installed_t gvdevice_types_xlib[] = {
#ifdef CAIRO_HAS_XLIB_SURFACE
    {0, "xlib:cairo", 0, &device_engine_xlib, &device_features_xlib},
    {0, "x11:cairo", 0, &device_engine_xlib, &device_features_xlib},
#endif
    {0, NULL, 0, NULL, NULL}};
