/* xscreensaver, Copyright © 2016-2021 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

/* JWXYZ Is Not Xlib.

   See the comment at the top of jwxyz-common.c for an explanation of
   the division of labor between these various modules.

   This file is three related things:
  
     - It is the Android-specific companion to jwxyz-gl.c or jwxyz-image.c;
     - It is how C calls into Java to do things that OpenGL does not have
       access to without Java-based APIs;
     - It is how the jwxyz.java class calls into C to run the hacks.
 */

#ifdef HAVE_ANDROID /* whole file */

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <setjmp.h>

#define GL_GLEXT_PROTOTYPES

#include <GLES/gl.h>
#include <GLES/glext.h>
#ifdef HAVE_GLES3
# include <GLES3/gl3.h>
#endif
#include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <pthread.h>

#include "screenhackI.h"
#include "jwxyzI.h"
#include "jwzglesI.h"
#include "jwxyz-android.h"
#include "textclient.h"
#include "grabclient.h"
#include "pow2.h"


#undef countof
#define countof(x) (sizeof(x)/sizeof(*(x)))

extern struct xscreensaver_function_table *xscreensaver_function_table;

struct function_table_entry {
  const char *progname;
  struct xscreensaver_function_table *xsft;
};

#include "gen/function-table.h"

struct event_queue {
  XEvent event;
  struct event_queue *next;
};

static void send_queued_events (struct running_hack *rh);

const char *progname;
const char *progclass;
int mono_p = 0;

static JavaVM *global_jvm;
static jmp_buf jmp_target;

static double current_rotation = 0;

extern void check_gl_error (const char *type);

void
jwxyz_logv(Bool error, const char *fmt, va_list args)
{
  __android_log_vprint(error ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO,
                       "xscreensaver", fmt, args);

  /* The idea here is that if the device/emulator dies shortly after a log
     message, then waiting here for a short while should increase the odds
     that adb logcat can pick up the message before everything blows up. Of
     course, doing this means dumping a ton of messages will slow things down
     significantly.
  */
# if 0
  struct timespec ts;
  ts.tv_sec = 0;
  ts.tv_nsec = 25 * 1000000;
  nanosleep(&ts, NULL);
# endif
}

/* Handle an abort on Android
   TODO: Test that Android handles aborts properly
 */
void
jwxyz_abort (const char *fmt, ...)
{
  /* Send error to Android device log */
  if (!fmt || !*fmt)
    fmt = "abort";

  va_list args;
  va_start (args, fmt);
  jwxyz_logv(True, fmt, args);
  va_end (args);

  char buf[10240];
  va_start (args, fmt);
  vsprintf (buf, fmt, args);
  va_end (args);

  JNIEnv *env;
  (*global_jvm)->AttachCurrentThread (global_jvm, &env, NULL);

  if (! (*env)->ExceptionOccurred(env)) {
    // If there's already an exception queued, let's just go with that one.
    // Else, queue a Java exception to be thrown.
    (*env)->ThrowNew (env, (*env)->FindClass(env, "java/lang/RuntimeException"),
                      buf);
  }

  // Nonlocal exit out of the jwxyz code.
  longjmp (jmp_target, 1);
}


/* We get to keep live references to Java classes in use because the VM can
   unload a class that isn't being used, which invalidates field and method
   IDs.
   https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp17074
*/


// #### only need one var I think
static size_t classRefCount = 0;
static jobject globalRefjwxyz, globalRefIterable, globalRefIterator,
    globalRefMapEntry;

static jfieldID runningHackField;
static jmethodID iterableIterator, iteratorHasNext, iteratorNext;
static jmethodID entryGetKey, entryGetValue;

static pthread_mutex_t mutg = PTHREAD_MUTEX_INITIALIZER;

static void screenhack_do_fps (Display *, Window, fps_state *, void *);
static char *get_string_resource_window (Window window, char *name);


/* Also creates double-buffered windows. */
static void
create_pixmap (Window win, Drawable p)
{
  // See also:
  // https://web.archive.org/web/20140213220709/http://blog.vlad1.com/2010/07/01/how-to-go-mad-while-trying-to-render-to-a-texture/
  // https://software.intel.com/en-us/articles/using-opengl-es-to-accelerate-apps-with-legacy-2d-guis
  // https://www.khronos.org/registry/egl/extensions/ANDROID/EGL_ANDROID_image_native_buffer.txt

  Assert (p->frame.width, "p->frame.width");
  Assert (p->frame.height, "p->frame.height");

  if (win->window.rh->jwxyz_gl_p) {
    struct running_hack *rh = win->window.rh;

    if (rh->gl_fbo_p) {
      glGenTextures (1, &p->texture);
      glBindTexture (GL_TEXTURE_2D, p->texture);

      glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA,
                    to_pow2(p->frame.width), to_pow2(p->frame.height),
                    0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);

      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    } else {
      EGLint attribs[5];
      attribs[0] = EGL_WIDTH;
      attribs[1] = p->frame.width;
      attribs[2] = EGL_HEIGHT;
      attribs[3] = p->frame.height;
      attribs[4] = EGL_NONE;

      p->egl_surface = eglCreatePbufferSurface(rh->egl_display, rh->egl_config,
                                             attribs);
      Assert (p->egl_surface != EGL_NO_SURFACE,
              "XCreatePixmap: got EGL_NO_SURFACE");
    }
  } else {
    p->image_data = malloc (p->frame.width * p->frame.height * 4);
  }
}


static void
free_pixmap (struct running_hack *rh, Pixmap p)
{
  if (rh->jwxyz_gl_p) {
    if (rh->gl_fbo_p) {
      glDeleteTextures (1, &p->texture);
    } else {
      eglDestroySurface(rh->egl_display, p->egl_surface);
    }
  } else {
    free (p->image_data);
  }
}


static void
prepare_context (struct running_hack *rh)
{
  if (rh->egl_p) {
    /* TODO: Adreno recommends against doing this every frame. */
    Assert (eglMakeCurrent(rh->egl_display, rh->egl_surface, rh->egl_surface,
                           rh->egl_ctx),
            "eglMakeCurrent failed");
  }

    /* Don't set matrices here; set them when an Xlib call triggers
       jwxyz_bind_drawable/jwxyz_set_matrices.
     */
  if (rh->jwxyz_gl_p)
    rh->current_drawable = NULL;

  if (rh->xsft->visual == GL_VISUAL)
    jwzgles_make_current (rh->gles_state);
}


static void
get_egl_config_android(Window window, EGLDisplay *egl_display,
                       EGLConfig *egl_config)
{
# define R EGL_RED_SIZE
# define G EGL_GREEN_SIZE
# define B EGL_BLUE_SIZE
# define A EGL_ALPHA_SIZE
# define D EGL_DEPTH_SIZE
# define I EGL_BUFFER_SIZE
# define ST EGL_STENCIL_SIZE
  EGLint templates[][40] = {
    { R,8, G,8, B,8, A,8, D,8, ST,1, EGL_NONE }, /* rgba stencil */
    { R,8, G,8, B,8,      D,8, ST,1, EGL_NONE }, /* rgb  stencil */
    { R,4, G,4, B,4,      D,4, ST,1, EGL_NONE },
    { R,2, G,2, B,2,      D,2, ST,1, EGL_NONE },
    { R,8, G,8, B,8, A,8, D,8,       EGL_NONE }, /* rgba */
    { R,8, G,8, B,8,      D,8,       EGL_NONE }, /* rgb  */
    { R,4, G,4, B,4,      D,4,       EGL_NONE },
    { R,2, G,2, B,2,      D,2,       EGL_NONE },
    { R,1, G,1, B,1,      D,1,       EGL_NONE }  /* monochrome */
  };
  EGLint attrs[40];
  EGLint nconfig;
  int i, j, k, iter, pass;

  char *glsls = get_string_resource_window (window, "prefersGLSL");
  Bool glslp = (glsls && !strcasecmp(glsls, "true"));
  iter = (glslp ? 2 : 1);

  *egl_config = 0;
  for (pass = 0; pass < iter; pass++)
    {
      for (i = 0; i < countof(templates); i++)
        {
          for (j = 0, k = 0; templates[i][j] != EGL_NONE; j += 2)
            {
              attrs[k++] = templates[i][j];
              attrs[k++] = templates[i][j+1];
            }

          attrs[k++] = EGL_RENDERABLE_TYPE;
# ifdef HAVE_GLES3
          if (glslp && pass == 0)
            attrs[k++] = EGL_OPENGL_ES3_BIT;
          else
            attrs[k++] = EGL_OPENGL_ES_BIT;
# else
          attrs[k++] = EGL_OPENGL_ES_BIT;
# endif

          attrs[k++] = EGL_NONE;

          nconfig = -1;
          if (eglChooseConfig (egl_display, attrs, egl_config, 1, &nconfig)
              && nconfig == 1)
            break;
        }
      if (i < countof(templates))
        break;
    }
  Assert (*egl_config != 0, "no EGL config chosen");
#if 1
  {
    int i;
    const struct { int hexp; EGLint i; const char *s; } fields[] = {
      { 1, EGL_CONFIG_ID,		"config ID:"	 },
      { 1, EGL_CONFIG_CAVEAT,		"caveat:"	 },
      { 1, EGL_CONFORMANT,		"conformant:"	 },
      { 0, EGL_COLOR_BUFFER_TYPE,	"buffer type:"	 },
      { 0, EGL_RED_SIZE,		"color size:"	 },
      { 0, EGL_TRANSPARENT_RED_VALUE,	"transparency:"	 },
      { 0, EGL_BUFFER_SIZE,		"buffer size:"	 },
      { 0, EGL_DEPTH_SIZE,		"depth size:"	 },
      { 0, EGL_LUMINANCE_SIZE,	"lum size:"	 },
      { 0, EGL_STENCIL_SIZE,		"stencil size:"	 },
      { 0, EGL_ALPHA_MASK_SIZE,	"alpha mask:"	 },
      { 0, EGL_LEVEL,			"level:"	 },
      { 0, EGL_SAMPLES,		"samples:"	 },
      { 0, EGL_SAMPLE_BUFFERS,	"sample bufs:"	 },
      { 0, EGL_NATIVE_RENDERABLE,	"native render:" },
      { 1, EGL_NATIVE_VISUAL_TYPE,	"native type:"	 },
      { 1, EGL_RENDERABLE_TYPE,	"render type:"	 },
      { 0, EGL_SURFACE_TYPE,		"surface type:"	 },
      { 0, EGL_BIND_TO_TEXTURE_RGB,	"bind RGB:"	 },
      { 0, EGL_BIND_TO_TEXTURE_RGBA,	"bind RGBA:"	 },
      { 0, EGL_MAX_PBUFFER_WIDTH,	"buffer width:"	 },
      { 0, EGL_MAX_PBUFFER_HEIGHT,	"buffer height:" },
      { 0, EGL_MAX_PBUFFER_PIXELS,	"buffer pixels:" },
      { 0, EGL_MAX_SWAP_INTERVAL,	"max swap:"	 },
      { 0, EGL_MIN_SWAP_INTERVAL,	"min swap:"	 },
    };
    EGLint r=0, g=0, b=0, a=0, tt=0, tr=0, tg=0, tb=0;
    eglGetConfigAttrib (egl_display, *egl_config, EGL_RED_SIZE,   &r);
    eglGetConfigAttrib (egl_display, *egl_config, EGL_GREEN_SIZE, &g);
    eglGetConfigAttrib (egl_display, *egl_config, EGL_BLUE_SIZE,  &b);
    eglGetConfigAttrib (egl_display, *egl_config, EGL_ALPHA_SIZE, &a);
    eglGetConfigAttrib (egl_display, *egl_config,
                        EGL_TRANSPARENT_TYPE, &tt);
    eglGetConfigAttrib (egl_display, *egl_config,
                        EGL_TRANSPARENT_RED_VALUE,  &tr);
    eglGetConfigAttrib (egl_display, *egl_config,
                        EGL_TRANSPARENT_GREEN_VALUE,&tg);
    eglGetConfigAttrib (egl_display, *egl_config,
                        EGL_TRANSPARENT_BLUE_VALUE, &tb);
    for (i = 0; i < countof(fields); i++)
      {
        EGLint v = 0;
        char s[100];
        eglGetConfigAttrib (egl_display, *egl_config, fields[i].i, &v);
        if (fields[i].i == EGL_RED_SIZE)
          sprintf (s, "%d, %d, %d, %d", r, g, b, a);
        else if (fields[i].i == EGL_TRANSPARENT_RED_VALUE && tt != EGL_NONE)
          sprintf (s, "%d, %d, %d", tr, tg, tb);
        else if (fields[i].i == EGL_CONFIG_CAVEAT)
          strcpy (s, (v == EGL_NONE ? "none" :
                      v == EGL_SLOW_CONFIG ? "slow" :
# ifdef EGL_NON_CONFORMANT
                      v == EGL_NON_CONFORMANT ? "non-conformant" :
# endif
                      "???"));
        else if (fields[i].i == EGL_COLOR_BUFFER_TYPE)
          strcpy (s, (v == EGL_RGB_BUFFER ? "RGB" :
                      v == EGL_LUMINANCE_BUFFER ? "luminance" :
                      "???"));
        else if (fields[i].i == EGL_CONFORMANT ||
                 fields[i].i == EGL_RENDERABLE_TYPE)
          {
            sprintf (s, "0x%02x", v);
            if (v & EGL_OPENGL_BIT)     strcat (s, " OpenGL");
            if (v & EGL_OPENGL_ES_BIT)  strcat (s, " GLES-1.x");
            if (v & EGL_OPENGL_ES2_BIT) strcat (s, " GLES-2.0");
# ifdef EGL_OPENGL_ES3_BIT
            if (v & EGL_OPENGL_ES3_BIT) strcat (s, " GLES-3.0");
# endif
            if (v & EGL_OPENVG_BIT)     strcat (s, " OpenVG");
          }
        else if (fields[i].hexp)
          sprintf (s, "0x%02x", v);
        else if (v)
          sprintf (s, "%d", v);
        else
          *s = 0;

        if (*s) Log ("init:    EGL %-14s %s\n", fields[i].s, s);
      }
  }
#endif
}


static void
get_egl_context_android(Window window, EGLDisplay *egl_display,
                        EGLConfig *egl_config, EGLContext *egl_context)
{
  EGLint context_attribs[][3] = {
    { EGL_CONTEXT_CLIENT_VERSION, 1, EGL_NONE },
    { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }
  };
  EGLint *attrs;
  Bool glslp;
  int pass, iter;

# ifdef EGL_OPENGL_ES3_BIT
  char *glsls = get_string_resource_window (window, "prefersGLSL");
  glslp = (glsls && !strcasecmp(glsls, "true"));
  if (glslp)
    {
      EGLint renderable_type;
      eglGetConfigAttrib (egl_display, egl_config, EGL_RENDERABLE_TYPE,
                          &renderable_type);
      Bool gles3p = (renderable_type & EGL_OPENGL_ES3_BIT) != 0;
      glslp = glslp && gles3p;
    }
# else
  glslp = False;
# endif
  iter = (glslp ? 2 : 1);

  *egl_context = EGL_NO_CONTEXT;
  for (pass = 0; pass < iter; pass++)
    {
      if (glslp && pass == 0)
        attrs = context_attribs[1];
      else
        attrs = context_attribs[0];
      *egl_context = eglCreateContext (egl_display, egl_config,
                                       EGL_NO_CONTEXT, attrs);
      if (*egl_context != EGL_NO_CONTEXT)
        break;
    }

  Assert (*egl_context != EGL_NO_CONTEXT, "init: EGL_NO_CONTEXT");
}


// Initialized OpenGL and runs the screenhack's init function.
//
static void
doinit (jobject jwxyz_obj, struct running_hack *rh, JNIEnv *env,
        const struct function_table_entry *chosen,
        jobject defaults, jint w, jint h, jobject jni_surface)
{
  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.

  progname = chosen->progname;
  rh->xsft = chosen->xsft;
  rh->jni_env = env;
  rh->jobject = jwxyz_obj;  // update this every time we call into C

  (*env)->GetJavaVM (env, &global_jvm);

# undef ya_rand_init  // This is the one and only place it is allowed
  ya_rand_init (0);

  Window wnd = (Window) calloc(1, sizeof(*wnd));
  wnd->window.rh = rh;
  wnd->frame.width = w;
  wnd->frame.height = h;
  wnd->type = WINDOW;

  rh->window = wnd;
  progclass = rh->xsft->progclass;

  if ((*env)->ExceptionOccurred(env)) abort();

  // This has to come before resource processing. It does not do graphics.
  if (rh->xsft->setup_cb)
    rh->xsft->setup_cb(rh->xsft, rh->xsft->setup_arg);

  if ((*env)->ExceptionOccurred(env)) abort();

  // Load the defaults.
  // Unceremoniously stolen from [PrefsReader defaultsToDict:].

  jclass     c = (*env)->GetObjectClass (env, defaults);
  jmethodID  m = (*env)->GetMethodID (env, c, "put",
                 "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
  if (! m) abort();
  if ((*env)->ExceptionOccurred(env)) abort();

  const struct { const char *key, *val; } default_defaults[] = {
    { "doubleBuffer", "True" },
    { "multiSample",  "False" },
    { "texFontCacheSize", "30" },
    { "textMode", "date" },
    { "textURL",
      "https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss" },
    { "grabDesktopImages",  "True" },
    { "chooseRandomImages", "True" },
  };

  for (int i = 0; i < countof(default_defaults); i++) {
    const char *key = default_defaults[i].key;
    const char *val = default_defaults[i].val;
    char *key2 = malloc (strlen(progname) + strlen(key) + 2);
    strcpy (key2, progname);
    strcat (key2, "_");
    strcat (key2, key);

    // defaults.put(key2, val);
    jstring jkey = (*env)->NewStringUTF (env, key2);
    jstring jval = (*env)->NewStringUTF (env, val);
    (*env)->CallObjectMethod (env, defaults, m, jkey, jval);
    (*env)->DeleteLocalRef (env, jkey);
    (*env)->DeleteLocalRef (env, jval);
    // Log ("default0: \"%s\" = \"%s\"", key2, val);
    free (key2);
  }

  const char *const *defs = rh->xsft->defaults;
  while (*defs) {
    char *line = strdup (*defs);
    char *key, *val;
    key = line;
    while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
      key++;
    val = key;
    while (*val && *val != ':')
      val++;
    if (*val != ':') abort();
    *val++ = 0;
    while (*val == ' ' || *val == '\t')
      val++;

    unsigned long L = strlen(val);
    while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
      val[--L] = 0;

    char *key2 = malloc (strlen(progname) + strlen(key) + 2);
    strcpy (key2, progname);
    strcat (key2, "_");
    strcat (key2, key);

    // defaults.put(key2, val);
    jstring jkey = (*env)->NewStringUTF (env, key2);
    jstring jval = (*env)->NewStringUTF (env, val);
    (*env)->CallObjectMethod (env, defaults, m, jkey, jval);
    (*env)->DeleteLocalRef (env, jkey);
    (*env)->DeleteLocalRef (env, jval);
    // Log ("default: \"%s\" = \"%s\"", key2, val);
    free (key2);
    free (line);
    defs++;
  }

  (*env)->DeleteLocalRef (env, c);
  if ((*env)->ExceptionOccurred(env)) abort();


  /* Note: https://source.android.com/devices/graphics/arch-egl-opengl */

  /* jwxyz_gl_p controls which implementation of Pixmaps we are using.

     - jwxyz-image.c implements them in CPU RAM, and is used for Android GL
       hacks, and for kumppa, petri and slip, which are too slow otherwise.

     - jwxyz-gl.c implements them in terms of OpenGL textures, and is used
       for all other Android X11 hacks.

     Why two implemementations of Pixmaps for Android?

     - GL hacks don't tend to need much in the way of Xlib, and having a
       GL context to render Xlib alongside a GL context for rendering GL
       seemed like trouble.

     - X11 hacks render to a GL context because hardware acceleration tends
       to work well with Xlib geometric stuff.  Better framerates, lower
       power.
   */
  rh->jwxyz_gl_p =
    rh->xsft->visual == DEFAULT_VISUAL &&
    strcmp (progname, "kumppa") &&
    strcmp (progname, "petri") &&
    strcmp (progname, "slip") &&
    strcmp (progname, "testx11");

  Log ("init: %s @ %dx%d: using JWXYZ_%s", progname, w, h,
       rh->jwxyz_gl_p ? "GL" : "IMAGE");

  rh->egl_p = rh->jwxyz_gl_p || rh->xsft->visual == GL_VISUAL;

  int egl_major = -1, egl_minor = -1;

  if (rh->egl_p) {
  // GL init. Must come after resource processing.

    rh->egl_display = eglGetDisplay (EGL_DEFAULT_DISPLAY);
    Assert (rh->egl_display != EGL_NO_DISPLAY, "init: EGL_NO_DISPLAY");

    Assert (eglInitialize (rh->egl_display, &egl_major, &egl_minor),
            "eglInitialize failed");

    get_egl_config_android (rh->window, rh->egl_display, &rh->egl_config);

    get_egl_context_android(rh->window, rh->egl_display, rh->egl_config,
                            &rh->egl_ctx);

    ANativeWindow *native_window =
      ANativeWindow_fromSurface (env, jni_surface);

    rh->egl_surface = eglCreateWindowSurface (rh->egl_display, rh->egl_config,
                                              native_window, NULL);
    Assert (rh->egl_surface != EGL_NO_SURFACE, "init: EGL_NO_SURFACE");

    ANativeWindow_release (native_window);
  } else {
    rh->native_window = ANativeWindow_fromSurface (env, jni_surface);

    int result = ANativeWindow_setBuffersGeometry (rh->native_window, w, h,
                                                   WINDOW_FORMAT_RGBX_8888);
    if (result < 0) {
      // Maybe check this earlier?
      Log ("can't set format (%d), surface may be invalid.", result);
      (*env)->ThrowNew (env,
        (*env)->FindClass(env, "org/jwz/xscreensaver/jwxyz$SurfaceLost"),
        "Surface lost");

      ANativeWindow_release (rh->native_window);
      rh->native_window = NULL;
      return;
    }
  }

  prepare_context (rh);

  if (rh->egl_p) {
    // GL_SHADING_LANGUAGE_VERSION undefined
    Log ("init %s / %s / %s / EGL %d.%d",
         glGetString (GL_VENDOR),
         glGetString (GL_RENDERER),
         glGetString (GL_VERSION),
         egl_major, egl_minor);
  }

  if (rh->jwxyz_gl_p) {
    const GLubyte *extensions = glGetString (GL_EXTENSIONS);
    rh->gl_fbo_p = jwzgles_gluCheckExtension (
      (const GLubyte *)"GL_OES_framebuffer_object", extensions);

    if (rh->gl_fbo_p) {
      glGetIntegerv (GL_FRAMEBUFFER_BINDING_OES, &rh->fb_default);
      Assert (!rh->fb_default, "default framebuffer not current framebuffer");
      glGenFramebuffersOES (1, &rh->fb_pixmap);
      wnd->texture = 0;
    } else {
      wnd->egl_surface = rh->egl_surface;
    }

    rh->frontbuffer_p = False;

    if (rh->xsft->visual == DEFAULT_VISUAL ||
        (rh->xsft->visual == GL_VISUAL &&
         strcmp("True", get_string_resource_window(wnd, "doubleBuffer")))) {

      rh->frontbuffer_p = True;

# if 0 /* Might need to be 0 for Adreno...? */
      if (egl_major > 1 || (egl_major == 1 && egl_minor >= 2)) {
        EGLint surface_type;
        eglGetConfigAttrib(rh->egl_display, rh->egl_config, EGL_SURFACE_TYPE,
                           &surface_type);
        if(surface_type & EGL_SWAP_BEHAVIOR_PRESERVED_BIT) {
          eglSurfaceAttrib(rh->egl_display, rh->egl_surface, EGL_SWAP_BEHAVIOR,
                           EGL_BUFFER_PRESERVED);
          rh->frontbuffer_p = False;
        }
      }
# endif

      if (rh->frontbuffer_p) {
        /* create_pixmap needs rh->gl_fbo_p and wnd->frame. */
        create_pixmap (wnd, wnd);

        /* No preserving backbuffers, so manual blit from back to "front". */
        rh->frontbuffer.type = PIXMAP;
        rh->frontbuffer.frame = wnd->frame;
        rh->frontbuffer.pixmap.depth = visual_depth (NULL, NULL);

        if(rh->gl_fbo_p) {
          rh->frontbuffer.texture = 0;
        } else {
          Assert (wnd->egl_surface != rh->egl_surface,
                  "oops: wnd->egl_surface == rh->egl_surface");
          rh->frontbuffer.egl_surface = rh->egl_surface;
        }
      }
    }

    rh->dpy = jwxyz_gl_make_display(wnd);

  } else {

    create_pixmap (wnd, wnd);

    static const unsigned char rgba_bytes[] = {0, 1, 2, 3};
    rh->dpy = jwxyz_image_make_display(wnd, rgba_bytes);

  }

  Assert(wnd == XRootWindow(rh->dpy, 0), "Wrong root window.");
  // TODO: Zero looks right, but double-check that is the right number

  /* Requires valid rh->dpy. */
  if (rh->jwxyz_gl_p)
    rh->copy_gc = XCreateGC (rh->dpy, &rh->frontbuffer, 0, NULL);

  if (rh->xsft->visual == GL_VISUAL)
    rh->gles_state = jwzgles_make_state();
 END: ;
}


#undef DEBUG_FPS

#ifdef DEBUG_FPS

static double
double_time (void)
{
  struct timeval now;
# ifdef GETTIMEOFDAY_TWO_ARGS
  struct timezone tzp;
  gettimeofday(&now, &tzp);
# else
  gettimeofday(&now);
# endif

  return (now.tv_sec + ((double) now.tv_usec * 0.000001));
}

#endif

// Animates a single frame of the current hack.
//
static jlong
drawXScreenSaver (JNIEnv *env, struct running_hack *rh)
{
# ifdef DEBUG_FPS
  double fps0=0, fps1=0, fps2=0, fps3=0, fps4=0;
  fps0 = fps1 = fps2 = fps3 = fps4 = double_time();
# endif

  unsigned long delay = 0;

  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.

  Window wnd = rh->window;

  prepare_context (rh);

  if (rh->egl_p) {
    /* There is some kind of weird redisplay race condition between Settings
       and the launching hack: e.g., Abstractile does XClearWindow at init,
       but the screen is getting filled with random bits.  So let's wait a
       few frames before really starting up.

       TODO: Is this still true?
     */
    if (++rh->frame_count < 8) {
      /* glClearColor (1.0, 0.0, 1.0, 0.0); */
      glClear (GL_COLOR_BUFFER_BIT); /* We always need to draw *something*. */
      goto END;
    }
  }

# ifdef DEBUG_FPS
  fps1 = double_time();
# endif

  // The init function might do graphics (e.g. XClearWindow) so it has
  // to be run from inside onDrawFrame, not onSurfaceChanged.

  if (! rh->initted_p) {

    void *(*init_cb) (Display *, Window, void *) =
      (void *(*)(Display *, Window, void *)) rh->xsft->init_cb;

    if (rh->xsft->visual == DEFAULT_VISUAL) {
      unsigned int bg =
        get_pixel_resource (rh->dpy, 0, "background", "Background");
      XSetWindowBackground (rh->dpy, wnd, bg);
      XClearWindow (rh->dpy, wnd);
    }

    rh->closure = init_cb (rh->dpy, wnd, rh->xsft->setup_arg);
    rh->initted_p = True;

    /* ignore_rotation_p doesn't quite work at the moment. */
    rh->ignore_rotation_p = False;
/*
      (rh->xsft->visual == DEFAULT_VISUAL &&
       get_boolean_resource (rh->dpy, "ignoreRotation", "IgnoreRotation"));
*/

    if (get_boolean_resource (rh->dpy, "doFPS", "DoFPS")) {
      rh->fpst = fps_init (rh->dpy, wnd);
      if (! rh->xsft->fps_cb) rh->xsft->fps_cb = screenhack_do_fps;
    } else {
      rh->fpst = NULL;
    }

    if ((*env)->ExceptionOccurred(env)) abort();
  }

# ifdef DEBUG_FPS
  fps2 = double_time();
# endif

  // Apparently events don't come in on the drawing thread, and JNI flips
  // out.  So we queue them there and run them here.
  // TODO: Events should be coming in on the drawing thread now, so dump this.
  send_queued_events (rh);

# ifdef DEBUG_FPS
  fps3 = double_time();
# endif

  delay = rh->xsft->draw_cb(rh->dpy, wnd, rh->closure);

  if (rh->jwxyz_gl_p)
    jwxyz_gl_flush (rh->dpy);

# ifdef DEBUG_FPS
  fps4 = double_time();
# endif
  if (rh->fpst && rh->xsft->fps_cb)
    rh->xsft->fps_cb (rh->dpy, wnd, rh->fpst, rh->closure);

  if (rh->egl_p) {
    if (rh->jwxyz_gl_p && rh->frontbuffer_p) {
      jwxyz_gl_copy_area (rh->dpy, wnd, &rh->frontbuffer, rh->copy_gc,
                          0, 0, wnd->frame.width, wnd->frame.height,
                          0, 0);
    }

    // Getting failure here before/during/after resize, sometimes. Log sez:
    // W/Adreno-EGLSUB(18428): <DequeueBuffer:607>: dequeue native buffer fail: No such device, buffer=0x5f93bf5c, handle=0x0
    if (!eglSwapBuffers(rh->egl_display, rh->egl_surface)) {
      Log ("eglSwapBuffers failed: 0x%x (probably asynchronous resize)",
           eglGetError());
    }
  } else {
    ANativeWindow_Buffer buffer;
    ARect rect = {0, 0, wnd->frame.width, wnd->frame.height};
    int32_t result = ANativeWindow_lock(rh->native_window, &buffer, &rect);
    if (result) {
      Log ("ANativeWindow_lock failed (result = %d), frame dropped", result);
    } else {
      /* Android can resize surfaces asynchronously. */
      if (wnd->frame.width != buffer.width ||
          wnd->frame.height != buffer.height) {
        Log ("buffer/window size mismatch: %dx%d (format = %d), wnd: %dx%d",
             buffer.width, buffer.height, buffer.format,
             wnd->frame.width, wnd->frame.height);
      }

      Assert (buffer.format == WINDOW_FORMAT_RGBA_8888 ||
              buffer.format == WINDOW_FORMAT_RGBX_8888,
              "bad buffer format");

      jwxyz_blit (wnd->image_data, jwxyz_image_pitch (wnd), 0, 0,
                  buffer.bits, buffer.stride * 4, 0, 0,
                  MIN(wnd->frame.width, buffer.width),
                  MIN(wnd->frame.height, buffer.height));
      // TODO: Clear any area to sides and bottom.

      ANativeWindow_unlockAndPost (rh->native_window);
    }
  }

 END: ;

# ifdef DEBUG_FPS
  Log("## FPS prep = %-6d init = %-6d events = %-6d draw = %-6d fps = %-6d\n",
      (int) ((fps1-fps0)*1000000),
      (int) ((fps2-fps1)*1000000),
      (int) ((fps3-fps2)*1000000),
      (int) ((fps4-fps3)*1000000),
      (int) ((double_time()-fps4)*1000000));
# endif

  return delay;
}


// Extracts the C structure that is stored in the jwxyz Java object.
static struct running_hack *
getRunningHack (JNIEnv *env, jobject thiz)
{
  jlong result = (*env)->GetLongField (env, thiz, runningHackField);
  struct running_hack *rh = (struct running_hack *)(intptr_t)result;
  if (rh)
    rh->jobject = thiz;  // update this every time we call into C
  return rh;
}

// Look up a class and mark it global in the provided variable.
static jclass
acquireClass (JNIEnv *env, const char *className, jobject *globalRef)
{
  jclass clazz = (*env)->FindClass(env, className);
  *globalRef = (*env)->NewGlobalRef(env, clazz);
  return clazz;
}


/* Note: to find signature strings for native methods:
   cd ./project/xscreensaver/build/intermediates/classes/debug/
   javap -s -p org.jwz.xscreensaver.jwxyz
 */


// Implementation of jwxyz's nativeInit Java method.
//
JNIEXPORT void JNICALL
Java_org_jwz_xscreensaver_jwxyz_nativeInit (JNIEnv *env, jobject thiz,
                                            jstring jhack, jobject defaults,
                                            jint w, jint h,
                                            jobject jni_surface)
{
  pthread_mutex_lock(&mutg);

  struct running_hack *rh = calloc(1, sizeof(struct running_hack));

  if ((*env)->ExceptionOccurred(env)) abort();

  // #### simplify
  if (!classRefCount) {
    jclass classjwxyz = (*env)->GetObjectClass(env, thiz);
    globalRefjwxyz = (*env)->NewGlobalRef(env, classjwxyz);
    runningHackField = (*env)->GetFieldID
      (env, classjwxyz, "nativeRunningHackPtr", "J");
    if ((*env)->ExceptionOccurred(env)) abort();

    jclass classIterable =
      acquireClass(env, "java/lang/Iterable", &globalRefIterable);
    iterableIterator = (*env)->GetMethodID
      (env, classIterable, "iterator", "()Ljava/util/Iterator;");
    if ((*env)->ExceptionOccurred(env)) abort();

    jclass classIterator =
      acquireClass(env, "java/util/Iterator", &globalRefIterator);
    iteratorHasNext = (*env)->GetMethodID
      (env, classIterator, "hasNext", "()Z");
    iteratorNext = (*env)->GetMethodID
      (env, classIterator, "next", "()Ljava/lang/Object;");
    if ((*env)->ExceptionOccurred(env)) abort();

    jclass classMapEntry =
      acquireClass(env, "java/util/Map$Entry", &globalRefMapEntry);
    entryGetKey = (*env)->GetMethodID
      (env, classMapEntry, "getKey", "()Ljava/lang/Object;");
    entryGetValue = (*env)->GetMethodID
      (env, classMapEntry, "getValue", "()Ljava/lang/Object;");
    if ((*env)->ExceptionOccurred(env)) abort();
  }

  ++classRefCount;

  // Store the C struct into the Java object.
  (*env)->SetLongField(env, thiz, runningHackField, (jlong)(intptr_t)rh);

  // TODO: Sort the list so binary search works.
  const char *hack =(*env)->GetStringUTFChars(env, jhack, NULL);

  int chosen = 0;
  for (;;) {
    if (chosen == countof(function_table)) {
      Log ("Hack not found: %s", hack);
      abort();
    }
    if (!strcmp(function_table[chosen].progname, hack))
      break;
    chosen++;
  }

  (*env)->ReleaseStringUTFChars(env, jhack, hack);

  doinit (thiz, rh, env, &function_table[chosen], defaults, w, h,
          jni_surface);

  pthread_mutex_unlock(&mutg);
}


JNIEXPORT void JNICALL
Java_org_jwz_xscreensaver_jwxyz_nativeResize (JNIEnv *env, jobject thiz,
                                              jint w, jint h, jdouble rot)
{
  pthread_mutex_lock(&mutg);
  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.

  current_rotation = rot;

  Log ("native rotation: %f", current_rotation);

  struct running_hack *rh = getRunningHack(env, thiz);

  prepare_context (rh);

  if (rh->egl_p) {
    glViewport (0, 0, w, h);
  } else {
    int result = ANativeWindow_setBuffersGeometry (rh->native_window, w, h,
                                                   WINDOW_FORMAT_RGBX_8888);
    if (result < 0)
      Log ("failed to resize surface (%d)", result);
  }

  Window wnd = rh->window;
  wnd->frame.x = 0;
  wnd->frame.y = 0;
  wnd->frame.width  = w;
  wnd->frame.height = h;

  if (ignore_rotation_p(rh->dpy) &&
      rot != 0 && rot != 180 && rot != -180) {
    int swap = w;
    w = h;
    h = swap;
    wnd->frame.width  = w;
    wnd->frame.height = h;
  }

  if (rh->jwxyz_gl_p) {
    if (rh->frontbuffer_p) {
      free_pixmap (rh, wnd);
      create_pixmap (wnd, wnd);

      rh->frontbuffer.frame = wnd->frame;
      if (!rh->gl_fbo_p)
        rh->frontbuffer.egl_surface = rh->egl_surface;
    }

    jwxyz_window_resized (rh->dpy);
  } else {
    free_pixmap (rh, wnd);
    create_pixmap (wnd, wnd);
    XClearWindow (rh->dpy, wnd); // TODO: This is lame. Copy the bits.
  }

  if (rh->initted_p)
    rh->xsft->reshape_cb (rh->dpy, rh->window, rh->closure,
                          wnd->frame.width, wnd->frame.height);

  if (rh->xsft->visual == GL_VISUAL) {
    glMatrixMode (GL_PROJECTION);
    glRotatef (-rot, 0, 0, 1);
    glMatrixMode (GL_MODELVIEW);
  }

 END:
  pthread_mutex_unlock(&mutg);
}


JNIEXPORT jlong JNICALL
Java_org_jwz_xscreensaver_jwxyz_nativeRender (JNIEnv *env, jobject thiz)
{
  pthread_mutex_lock(&mutg);
  struct running_hack *rh = getRunningHack(env, thiz);
  jlong result = drawXScreenSaver(env, rh);
  pthread_mutex_unlock(&mutg);
  return result;
}


// TODO: Check Java side is calling this properly
JNIEXPORT void JNICALL
Java_org_jwz_xscreensaver_jwxyz_nativeDone (JNIEnv *env, jobject thiz)
{
  pthread_mutex_lock(&mutg);
  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.

  struct running_hack *rh = getRunningHack(env, thiz);

  prepare_context (rh);

  if (rh->fpst)
    rh->xsft->fps_free (rh->fpst);
  if (rh->initted_p)
    rh->xsft->free_cb (rh->dpy, rh->window, rh->closure);
  if (rh->jwxyz_gl_p)
    XFreeGC (rh->dpy, rh->copy_gc);
  if (rh->xsft->visual == GL_VISUAL)
    jwzgles_free_state ();

  if (rh->jwxyz_gl_p)
    jwxyz_gl_free_display(rh->dpy);
  else
    jwxyz_image_free_display(rh->dpy);

  if (rh->egl_p) {
    // eglDestroy* probably isn't necessary here.
    eglMakeCurrent (rh->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
                    EGL_NO_CONTEXT);
    eglDestroySurface (rh->egl_display, rh->egl_surface);
    eglDestroyContext (rh->egl_display, rh->egl_ctx);
    eglTerminate (rh->egl_display);
  } else {
    free_pixmap (rh, rh->window);
    if (rh->native_window)
      ANativeWindow_release (rh->native_window);
  }

  free(rh);
  (*env)->SetLongField(env, thiz, runningHackField, 0);

  --classRefCount;
  if (!classRefCount) {
    (*env)->DeleteGlobalRef(env, globalRefjwxyz);
    (*env)->DeleteGlobalRef(env, globalRefIterable);
    (*env)->DeleteGlobalRef(env, globalRefIterator);
    (*env)->DeleteGlobalRef(env, globalRefMapEntry);
  }

 END:
  pthread_mutex_unlock(&mutg);
}


static int
send_event (struct running_hack *rh, XEvent *e)
{
  // Assumes mutex is locked and context is prepared

  int *xP = 0, *yP = 0;
  switch (e->xany.type) {
  case ButtonPress: case ButtonRelease:
    xP = &e->xbutton.x;
    yP = &e->xbutton.y;
    break;
  case MotionNotify:
    xP = &e->xmotion.x;
    yP = &e->xmotion.y;
    break;
  }

  // Rotate the coordinates in the events to match the pixels.
  if (xP) {
    if (ignore_rotation_p (rh->dpy)) {
      Window win = XRootWindow (rh->dpy, 0);
      int w = win->frame.width;
      int h = win->frame.height;
      int swap;
      switch ((int) current_rotation) {
      case 180: case -180:				// #### untested
        *xP = w - *xP;
        *yP = h - *yP;
        break;
      case 90: case -270:
        swap = *xP; *xP = *yP; *yP = swap;
        *yP = h - *yP;
        break;
      case -90: case 270:				// #### untested
        swap = *xP; *xP = *yP; *yP = swap;
        *xP = w - *xP;
        break;
      }
    }

    rh->window->window.last_mouse_x = *xP;
    rh->window->window.last_mouse_y = *yP;
  }

  return (rh->xsft->event_cb
          ? rh->xsft->event_cb (rh->dpy, rh->window, rh->closure, e)
          : 0);
}


static void
send_queued_events (struct running_hack *rh)
{
  struct event_queue *event, *next;
  if (! rh->event_queue) return;
  for (event = rh->event_queue, next = event->next;
       event;
       event = next, next = (event ? event->next : 0)) {
    if (! send_event (rh, &event->event)) {
      // #### flash the screen or something
    }
    free (event);
  }
  rh->event_queue = 0;
}


static void
queue_event (JNIEnv *env, jobject thiz, XEvent *e)
{
  pthread_mutex_lock (&mutg);
  struct running_hack *rh = getRunningHack (env, thiz);
  struct event_queue *q = (struct event_queue *) malloc (sizeof(*q));
  memcpy (&q->event, e, sizeof(*e));
  q->next = 0;

  // Put it at the end.
  struct event_queue *oq;
  for (oq = rh->event_queue; oq && oq->next; oq = oq->next)
    ;
  if (oq)
    oq->next = q;
  else
    rh->event_queue = q;

  pthread_mutex_unlock (&mutg);
}


JNIEXPORT void JNICALL
Java_org_jwz_xscreensaver_jwxyz_sendButtonEvent (JNIEnv *env, jobject thiz,
                                                 int x, int y, jboolean down)
{
  XEvent e;
  memset (&e, 0, sizeof(e));
  e.xany.type = (down ? ButtonPress : ButtonRelease);
  e.xbutton.button = Button1;
  e.xbutton.x = x;
  e.xbutton.y = y;
  queue_event (env, thiz, &e);
}

JNIEXPORT void JNICALL
Java_org_jwz_xscreensaver_jwxyz_sendMotionEvent (JNIEnv *env, jobject thiz,
                                                 int x, int y)
{
  XEvent e;
  memset (&e, 0, sizeof(e));
  e.xany.type = MotionNotify;
  e.xmotion.x = x;
  e.xmotion.y = y;
  queue_event (env, thiz, &e);
}

JNIEXPORT void JNICALL
Java_org_jwz_xscreensaver_jwxyz_sendKeyEvent (JNIEnv *env, jobject thiz,
                                              jboolean down_p, 
                                              int code, int mods)
{
  XEvent e;
  memset (&e, 0, sizeof(e));
  e.xkey.keycode = code;
  e.xkey.state = code;
  e.xany.type = (down_p ? KeyPress : KeyRelease);
  queue_event (env, thiz, &e);
  e.xany.type = KeyRelease;
  queue_event (env, thiz, &e);
}


/***************************************************************************
  Backend functions for jwxyz-gl.c
 */

static void
finish_bind_drawable (Display *dpy, Drawable dst)
{
  jwxyz_assert_gl ();

  glViewport (0, 0, dst->frame.width, dst->frame.height);
  jwxyz_set_matrices (dpy, dst->frame.width, dst->frame.height, False);
}


static void
bind_drawable_fbo (struct running_hack *rh, Drawable d)
{
  glBindFramebufferOES (GL_FRAMEBUFFER_OES,
                        d->texture ? rh->fb_pixmap : rh->fb_default);
  if (d->texture) {
    glFramebufferTexture2DOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
                               GL_TEXTURE_2D, d->texture, 0);
  }
}


void
jwxyz_bind_drawable (Display *dpy, Window w, Drawable d)
{
  struct running_hack *rh = w->window.rh;
  JNIEnv *env = w->window.rh->jni_env;
  if ((*env)->ExceptionOccurred(env)) abort();
  if (rh->current_drawable != d) {
    if (rh->gl_fbo_p) {
      bind_drawable_fbo (rh, d);
    } else {
      eglMakeCurrent (rh->egl_display, d->egl_surface, d->egl_surface, rh->egl_ctx);
    }
    finish_bind_drawable (dpy, d);
    rh->current_drawable = d;
  }
}

void
jwxyz_gl_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc,
                    int src_x, int src_y,
                    unsigned int width, unsigned int height,
                    int dst_x, int dst_y)
{
  Window w = XRootWindow (dpy, 0);
  struct running_hack *rh = w->window.rh;

  jwxyz_gl_flush (dpy);

  if (rh->gl_fbo_p && src->texture && src != dst) {
    bind_drawable_fbo (rh, dst);
    finish_bind_drawable (dpy, dst);
    rh->current_drawable = NULL;

    jwxyz_gl_set_gc (dpy, gc);

    glBindTexture (GL_TEXTURE_2D, src->texture);

    jwxyz_gl_draw_image (dpy, gc, GL_TEXTURE_2D, to_pow2(src->frame.width),
                         to_pow2(src->frame.height),
                         src_x, src->frame.height - src_y - height,
                         jwxyz_drawable_depth (src), width, height,
                         dst_x, dst_y, False);
    return;
  }

#if 1
  // Kumppa: 0.24 FPS
  // Hilarious display corruption ahoy! (Note to self: it's on the emulator.)
  // TODO for Dave: Recheck behavior on the emulator with the better Pixmap support.

  rh->current_drawable = NULL;
  if (rh->gl_fbo_p)
    bind_drawable_fbo (rh, src);
  else
    eglMakeCurrent (rh->egl_display, dst->egl_surface, src->egl_surface, rh->egl_ctx);

  jwxyz_gl_copy_area_read_tex_image (dpy, src->frame.height, src_x, src_y,
                                     width, height, dst_x, dst_y);

  if (rh->gl_fbo_p)
    bind_drawable_fbo (rh, dst);
  finish_bind_drawable (dpy, dst);

  jwxyz_gl_copy_area_write_tex_image (dpy, gc, src_x, src_y,
                                      jwxyz_drawable_depth (src),
                                      width, height, dst_x, dst_y);

#else
  // Kumppa: 0.17 FPS
  jwxyz_gl_copy_area_read_pixels (dpy, src, dst, gc, src_x, src_y,
                                  width, height, dst_x, dst_y);
#endif
  jwxyz_assert_gl ();
}


void
jwxyz_assert_drawable (Window main_window, Drawable d)
{
  check_gl_error("jwxyz_assert_drawable");
}


void
jwxyz_assert_gl (void)
{
  check_gl_error("jwxyz_assert_gl");
}


/***************************************************************************
  Backend functions for jwxyz-image.c
 */

ptrdiff_t
jwxyz_image_pitch (Drawable d)
{
  return d->frame.width * 4;
}

void *
jwxyz_image_data (Drawable d)
{
  Assert (d->image_data, "no image storage (i.e. keep Xlib off the Window)");
  return d->image_data;
}


const XRectangle *
jwxyz_frame (Drawable d)
{
  return &d->frame;
}


unsigned int
jwxyz_drawable_depth (Drawable d)
{
  return (d->type == WINDOW
          ? visual_depth (NULL, NULL)
          : d->pixmap.depth);
}


void
jwxyz_get_pos (Window w, XPoint *xvpos, XPoint *xp)
{
  xvpos->x = 0;
  xvpos->y = 0;

  if (xp) {
    xp->x = w->window.last_mouse_x;
    xp->y = w->window.last_mouse_y;
  }
}


static void
screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
{
  fps_compute (fpst, 0, -1);
  fps_draw (fpst);
}


Pixmap
XCreatePixmap (Display *dpy, Drawable d,
               unsigned int width, unsigned int height, unsigned int depth)
{
  Window win = XRootWindow(dpy, 0);

  Pixmap p = malloc(sizeof(*p));
  p->type = PIXMAP;
  p->frame.x = 0;
  p->frame.y = 0;
  p->frame.width = width;
  p->frame.height = height;

  Assert(depth == 1 || depth == visual_depth(NULL, NULL),
         "XCreatePixmap: bad depth");
  p->pixmap.depth = depth;

  create_pixmap (win, p);

  /* For debugging. */
# if 0
  jwxyz_bind_drawable (dpy, win, p);
  glClearColor (frand(1), frand(1), frand(1), 0);
  glClear (GL_COLOR_BUFFER_BIT);
# endif

  return p;
}


int
XFreePixmap (Display *d, Pixmap p)
{
  struct running_hack *rh = XRootWindow(d, 0)->window.rh;

  if (rh->jwxyz_gl_p) {
    jwxyz_gl_flush (d);

    if (rh->current_drawable == p)
      rh->current_drawable = NULL;
  }

  free_pixmap (rh, p);
  free (p);
  return 0;
}


double
current_device_rotation (void)
{
  return current_rotation;
}

Bool
ignore_rotation_p (Display *dpy)
{
  struct running_hack *rh = XRootWindow(dpy, 0)->window.rh;
  return rh->ignore_rotation_p;
}


static char *
jstring_dup (JNIEnv *env, jstring str)
{
  Assert (str, "expected jstring, not null");
  const char *cstr = (*env)->GetStringUTFChars (env, str, 0);
  size_t len = (*env)->GetStringUTFLength (env, str) + 1;
  char *result = malloc (len);
  if (result) {
    memcpy (result, cstr, len);
  }
  (*env)->ReleaseStringUTFChars (env, str, cstr);
  return result;
}


static char *
get_string_resource_window (Window window, char *name)
{
  JNIEnv *env = window->window.rh->jni_env;
  jobject obj = window->window.rh->jobject;

  if ((*env)->ExceptionOccurred(env)) abort();
  jstring jstr = (*env)->NewStringUTF (env, name);
  jclass     c = (*env)->GetObjectClass (env, obj);
  jmethodID  m = (*env)->GetMethodID (env, c, "getStringResource",
                           "(Ljava/lang/String;)Ljava/lang/String;");
  if ((*env)->ExceptionOccurred(env)) abort();

  jstring jvalue = (m
                  ? (*env)->CallObjectMethod (env, obj, m, jstr)
                  : NULL);
  (*env)->DeleteLocalRef (env, c);
  (*env)->DeleteLocalRef (env, jstr);
  char *ret = 0;
  if (jvalue)
    ret = jstring_dup (env, jvalue);

  Log("pref %s = %s", name, (ret ? ret : "(null)"));
  return ret;
}


char *
get_string_resource (Display *dpy, char *name, char *class)
{
  return get_string_resource_window (RootWindow (dpy, 0), name);
}


/* Returns the contents of the URL. */
char *
textclient_mobile_url_string (Display *dpy, const char *url)
{
  Window window = RootWindow (dpy, 0);
  JNIEnv *env = window->window.rh->jni_env;
  jobject obj = window->window.rh->jobject;

  jstring jstr  = (*env)->NewStringUTF (env, url);
  jclass      c = (*env)->GetObjectClass (env, obj);
  jmethodID   m = (*env)->GetMethodID (env, c, "loadURL",
                            "(Ljava/lang/String;)Ljava/nio/ByteBuffer;");
  if ((*env)->ExceptionOccurred(env)) abort();
  jobject buf = (m
                 ? (*env)->CallObjectMethod (env, obj, m, jstr)
                 : NULL);
  (*env)->DeleteLocalRef (env, c);
  (*env)->DeleteLocalRef (env, jstr);

  char *body = (char *) (buf ? (*env)->GetDirectBufferAddress (env, buf) : 0);
  char *body2;
  if (body) {
    int L = (*env)->GetDirectBufferCapacity (env, buf);
    body2 = malloc (L + 1);
    memcpy (body2, body, L);
    body2[L] = 0;
  } else {
    body2 = strdup ("ERROR");
  }

  if (buf)
    (*env)->DeleteLocalRef (env, buf);

  return body2;
}


float
jwxyz_scale (Window main_window)
{
  // TODO: Use the actual device resolution.
  return 2;
}

float
jwxyz_font_scale (Window main_window)
{
  return jwxyz_scale (main_window);
}


const char *
jwxyz_default_font_family (int require)
{
  /* Font families in XLFDs are totally ignored (for now). */
  return "sans-serif";
}


void *
jwxyz_load_native_font (Window window,
                        int traits_jwxyz, int mask_jwxyz,
                        const char *font_name_ptr, size_t font_name_length,
                        int font_name_type, float size,
                        char **family_name_ret,
                        int *ascent_ret, int *descent_ret)
{
  JNIEnv *env = window->window.rh->jni_env;
  jobject obj = window->window.rh->jobject;

  jstring jname = NULL;
  if (font_name_ptr) {
    char *name_nul = malloc(font_name_length + 1);
    memcpy(name_nul, font_name_ptr, font_name_length);
    name_nul[font_name_length] = 0;
    jname = (*env)->NewStringUTF (env, name_nul);
    free(name_nul);
  }

  jclass     c = (*env)->GetObjectClass (env, obj);
  jmethodID  m = (*env)->GetMethodID (env, c, "loadFont",
                           "(IILjava/lang/String;IF)[Ljava/lang/Object;");
  if ((*env)->ExceptionOccurred(env)) abort();

  jobjectArray array = (m
                        ? (*env)->CallObjectMethod (env, obj, m, (jint)mask_jwxyz,
                                                    (jint)traits_jwxyz, jname,
                                                    (jint)font_name_type, (jfloat)size)
                        : NULL);

  (*env)->DeleteLocalRef (env, c);

  if (array) {
    jobject font = (*env)->GetObjectArrayElement (env, array, 0);
    jobject family_name =
      (jstring) ((*env)->GetObjectArrayElement (env, array, 1));
    jobject asc  = (*env)->GetObjectArrayElement (env, array, 2);
    jobject desc = (*env)->GetObjectArrayElement (env, array, 3);
    if ((*env)->ExceptionOccurred(env)) abort();

    if (family_name_ret)
      *family_name_ret = jstring_dup (env, family_name);

    jobject paint = (*env)->NewGlobalRef (env, font);
    if ((*env)->ExceptionOccurred(env)) abort();

    c = (*env)->GetObjectClass(env, asc);
    m = (*env)->GetMethodID (env, c, "floatValue", "()F");
    if ((*env)->ExceptionOccurred(env)) abort();

    *ascent_ret  = (int) (*env)->CallFloatMethod (env, asc,  m);
    *descent_ret = (int) (*env)->CallFloatMethod (env, desc, m);

    return (void *) paint;
  } else {
    return 0;
  }
}


void
jwxyz_release_native_font (Display *dpy, void *native_font)
{
  Window window = RootWindow (dpy, 0);
  JNIEnv *env = window->window.rh->jni_env;
  if ((*env)->ExceptionOccurred(env)) abort();
  (*env)->DeleteGlobalRef (env, (jobject) native_font);
  if ((*env)->ExceptionOccurred(env)) abort();
}


/* If the local reference table fills up, use this to figure out where
   you missed a call to DeleteLocalRef. */
/*
static void dump_reference_tables(JNIEnv *env)
{
  jclass c = (*env)->FindClass(env, "dalvik/system/VMDebug");
  jmethodID m = (*env)->GetStaticMethodID (env, c, "dumpReferenceTables",
                                           "()V");
  (*env)->CallStaticVoidMethod (env, c, m);
  (*env)->DeleteLocalRef (env, c);
}
*/


// Returns the metrics of the multi-character, single-line UTF8 or Latin1
// string.  If pixmap_ret is provided, also renders the text.
//
void
jwxyz_render_text (Display *dpy, void *native_font,
                   const char *str, size_t len, Bool utf8, Bool antialias_p,
                   XCharStruct *cs, char **pixmap_ret)
{
  Window window = RootWindow (dpy, 0);
  JNIEnv *env = window->window.rh->jni_env;
  jobject obj = window->window.rh->jobject;

  char *s2;

  if (utf8) {
    s2 = malloc (len + 1);
    memcpy (s2, str, len);
    s2[len] = 0;
  } else {	// Convert Latin1 to UTF8
    s2 = malloc (len * 2 + 1);
    unsigned char *s3 = (unsigned char *) s2;
    int i;
    for (i = 0; i < len; i++) {
      unsigned char c = ((unsigned char *) str)[i];
      if (! (c & 0x80)) {
        *s3++ = c;
      } else {
        *s3++ = (0xC0 | (0x03 & (c >> 6)));
        *s3++ = (0x80 | (0x3F & c));
      }
    }
    *s3 = 0;
  }

  jstring jstr  = (*env)->NewStringUTF (env, s2);
  jclass      c = (*env)->GetObjectClass (env, obj);
  jmethodID   m = (*env)->GetMethodID (env, c, "renderText",
    "(Landroid/graphics/Paint;Ljava/lang/String;ZZ)Ljava/nio/ByteBuffer;");
  if ((*env)->ExceptionOccurred(env)) abort();
  jobject buf =
    (m
     ? (*env)->CallObjectMethod (env, obj, m,
                                 (jobject) native_font,
                                 jstr,
                                 (pixmap_ret ? JNI_TRUE : JNI_FALSE),
                                 antialias_p)
     : NULL);
  (*env)->DeleteLocalRef (env, c);
  (*env)->DeleteLocalRef (env, jstr);
  free (s2);

  if ((*env)->ExceptionOccurred(env)) abort();
  unsigned char *bits = (unsigned char *)
    (buf ? (*env)->GetDirectBufferAddress (env, buf) : 0);
  if (bits) {
    int i = 0;
    int L = (*env)->GetDirectBufferCapacity (env, buf);
    if (L < 10) abort();
    cs->lbearing = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
    cs->rbearing = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
    cs->width    = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
    cs->ascent   = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
    cs->descent  = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;

    if (pixmap_ret) {
      char *pix = malloc (L - i);
      if (! pix) abort();
      memcpy (pix, bits + i, L - i);
      *pixmap_ret = pix;
    }
  } else {
    memset (cs, 0, sizeof(*cs));
    if (pixmap_ret)
      *pixmap_ret = 0;
  }

  if (buf)
    (*env)->DeleteLocalRef (env, buf);
}


// Returns the verbose Unicode name of this character, like "agrave" or
// "daggerdouble".  Used by Unicrud, and by Fontglide with debugMetrics.
//
char *
jwxyz_unicode_character_name (Display *dpy, Font fid, unsigned long uc)
{
  JNIEnv *env = XRootWindow (dpy, 0)->window.rh->jni_env;
  /* FindClass doesn't like to load classes if GetStaticMethodID fails. Huh? */
  jclass
    c = (*env)->FindClass (env, "java/lang/Character"),
    c2 = (*env)->FindClass (env, "java/lang/NoSuchMethodError");

  if ((*env)->ExceptionOccurred(env)) abort();
  jmethodID m = (*env)->GetStaticMethodID (
    env, c, "getName", "(I)Ljava/lang/String;");
  jthrowable exc = (*env)->ExceptionOccurred(env);
  if (exc) {
    if ((*env)->IsAssignableFrom(env, (*env)->GetObjectClass(env, exc), c2)) {
      (*env)->ExceptionClear (env);
      Assert (!m, "jwxyz_unicode_character_name: m?");
    } else {
      abort();
    }
  }

  char *ret = NULL;

  if (m) {
    jstring name = (*env)->CallStaticObjectMethod (env, c, m, (jint)uc);
    if (name)
     ret = jstring_dup (env, name);
  }

  if (!ret) {
    asprintf(&ret, "U+%.4lX", uc);
  }

  return ret;
}


/* Called from utils/grabclient.c */
char *
jwxyz_draw_random_image (Display *dpy, Drawable drawable, GC gc)
{
  Window window = RootWindow (dpy, 0);
  struct running_hack *rh = window->window.rh;
  JNIEnv *env = rh->jni_env;
  jobject obj = rh->jobject;

  Bool images_p =
    get_boolean_resource (rh->dpy, "chooseRandomImages", "ChooseRandomImages");
  Bool grab_p =
    get_boolean_resource (rh->dpy, "grabDesktopImages", "GrabDesktopImages");
  Bool rotate_p =
    get_boolean_resource (rh->dpy, "rotateImages", "RotateImages");

  if (!images_p && !grab_p)
    return 0;

  if (grab_p && images_p) {
    grab_p = !(random() & 5);    /* if both, screenshot 1/5th of the time */
    images_p = !grab_p;
  }

  jclass      c = (*env)->GetObjectClass (env, obj);
  jmethodID   m = (*env)->GetMethodID (env, c, 
                                       (grab_p
                                        ? "getScreenshot"
                                        : "checkThenLoadRandomImage"),
                                       "(IIZ)[Ljava/lang/Object;");
  if ((*env)->ExceptionOccurred(env)) abort();
  jobjectArray img_name = (
    m
    ? (*env)->CallObjectMethod (env, obj, m,
                                drawable->frame.width, drawable->frame.height,
                                (rotate_p ? JNI_TRUE : JNI_FALSE))
    : NULL);
  if ((*env)->ExceptionOccurred(env)) abort();
  (*env)->DeleteLocalRef (env, c);

  if (!img_name) {
    fprintf (stderr, "failed to load %s\n", (grab_p ? "screenshot" : "image"));
    return NULL;
  }

  jobject jbitmap = (*env)->GetObjectArrayElement (env, img_name, 0);

  AndroidBitmapInfo bmp_info;
  AndroidBitmap_getInfo (env, jbitmap, &bmp_info);

  XImage *img = XCreateImage (dpy, NULL, visual_depth(NULL, NULL),
                              ZPixmap, 0, NULL,
                              bmp_info.width, bmp_info.height, 0,
                              bmp_info.stride);

  AndroidBitmap_lockPixels (env, jbitmap, (void **) &img->data);

  XPutImage (dpy, drawable, gc, img, 0, 0,
             (drawable->frame.width  - bmp_info.width) / 2,
             (drawable->frame.height - bmp_info.height) / 2,
             bmp_info.width, bmp_info.height);

  AndroidBitmap_unlockPixels (env, jbitmap);
  img->data = NULL;
  XDestroyImage (img);

  return jstring_dup (env, (*env)->GetObjectArrayElement (env, img_name, 1));
}


XImage *
jwxyz_png_to_ximage (Display *dpy, Visual *visual,
                     const unsigned char *png_data, unsigned long data_size)
{
  Window window = RootWindow (dpy, 0);
  struct running_hack *rh = window->window.rh;
  JNIEnv *env = rh->jni_env;
  jobject obj = rh->jobject;
  jclass    c = (*env)->GetObjectClass (env, obj);
  jmethodID m = (*env)->GetMethodID (env, c, "decodePNG",
                                     "([B)Landroid/graphics/Bitmap;");
  if ((*env)->ExceptionOccurred(env)) abort();
  jbyteArray jdata = (*env)->NewByteArray (env, data_size);
  (*env)->SetByteArrayRegion (env, jdata, 0,
                              data_size, (const jbyte *) png_data);
  jobject jbitmap = (
    m
    ? (*env)->CallObjectMethod (env, obj, m, jdata)
    : NULL);
  if ((*env)->ExceptionOccurred(env)) abort();
  (*env)->DeleteLocalRef (env, c);
  (*env)->DeleteLocalRef (env, jdata);
  if (!jbitmap)
    return NULL;

  AndroidBitmapInfo bmp_info;
  AndroidBitmap_getInfo (env, jbitmap, &bmp_info);

  XImage *img = XCreateImage (dpy, NULL, 32, ZPixmap, 0, NULL,
                              bmp_info.width, bmp_info.height, 8,
                              bmp_info.stride);
  char *bits = 0;
  AndroidBitmap_lockPixels (env, jbitmap, (void **) &bits);
  img->data = (char *) calloc (img->bytes_per_line, img->height);
  memcpy (img->data, bits, img->bytes_per_line * img->height);
  AndroidBitmap_unlockPixels (env, jbitmap);

  // Java should have returned ARGB data.
  // WTF, why isn't ANDROID_BITMAP_FORMAT_ARGB_8888 defined?
  if (bmp_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) abort();
# ifndef __BYTE_ORDER__ // A GCC (and Clang)-ism.
#  error Need a __BYTE_ORDER__.
# elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
  img->byte_order = img->bitmap_bit_order = LSBFirst;
# elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
  img->byte_order = img->bitmap_bit_order = MSBFirst;
# else
#  error Need a __BYTE_ORDER__.
# endif

  static const union {
    uint8_t bytes[4];
    uint32_t pixel;
  } c0 = {{0xff, 0x00, 0x00, 0x00}}, c1 = {{0x00, 0xff, 0x00, 0x00}},
    c2 = {{0x00, 0x00, 0xff, 0x00}};

  img->red_mask   = c0.pixel;
  img->green_mask = c1.pixel;
  img->blue_mask  = c2.pixel;

  return img;
}

#endif /* HAVE_ANDROID */
