/*
 * Graphic driver for openKropki, using SDL as backend
 *
 * Copyright (C) 2014-2020 Mateusz Viste
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */


#include <stdio.h>    /* puts(), printf() */
#include <stdlib.h>   /* malloc(), free() */
#include <string.h>   /* memset */
#include <math.h>     /* sqrt() */
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>

#include "gz.h"
#include "drv_gra.h" /* include self for control */

#define SCALE 1

/* convenience functions to extract RGB values from a compound value */
#define RGB_R(x) ((x >> 16) & 0xff)
#define RGB_G(x) ((x >> 8) & 0xff)
#define RGB_B(x) (x & 0xff)


struct gra_sprite {
  SDL_Texture *ptr;
  int w;
  int h;
  unsigned char cached; /* 1 = owned by asset cache; gra_freesprite is a no-op */
};

#define BMP_CACHE_MAX 32
#define WAV_CACHE_MAX 16

static struct {
  const void *key;
  struct gra_sprite *spr;
} bmp_cache[BMP_CACHE_MAX];
static int bmp_cache_count = 0;

static struct {
  const void *key;
  Uint8 *buf;
  Uint32 len;
} wav_cache[WAV_CACHE_MAX];
static int wav_cache_count = 0;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;


static struct gra_sprite *bmp_cache_find(const void *key) {
  int i;
  for (i = 0; i < bmp_cache_count; i++) {
    if (bmp_cache[i].key == key) return(bmp_cache[i].spr);
  }
  return(NULL);
}


static void bmp_cache_add(const void *key, struct gra_sprite *spr) {
  if ((spr == NULL) || (bmp_cache_count >= BMP_CACHE_MAX)) return;
  spr->cached = 1;
  bmp_cache[bmp_cache_count].key = key;
  bmp_cache[bmp_cache_count].spr = spr;
  bmp_cache_count++;
}


static int wav_cache_find(const void *key, Uint8 **buf, Uint32 *len) {
  int i;
  for (i = 0; i < wav_cache_count; i++) {
    if (wav_cache[i].key == key) {
      if (buf != NULL) *buf = wav_cache[i].buf;
      if (len != NULL) *len = wav_cache[i].len;
      return(1);
    }
  }
  return(0);
}


/* gunzip + decode a wav.gz blob into the WAV cache (no playback). returns 0 on success. */
static int wav_cache_load(const void *memgz, long memgzlen) {
  unsigned char *rawwave;
  Uint8 *rawaudio;
  Uint32 rawaudiolen;
  long rawwavelen;
  SDL_AudioSpec *res, audiospec;
  SDL_RWops *rwop;

  if (memgz == NULL) return(-1);
  if (wav_cache_find(memgz, NULL, NULL)) return(0);
  if (wav_cache_count >= WAV_CACHE_MAX) return(-2);

  rawwave = ungz(memgz, memgzlen, &rawwavelen);
  if (rawwave == NULL) return(-3);
  rwop = SDL_RWFromMem(rawwave, (int)rawwavelen);
  res = SDL_LoadWAV_RW(rwop, 1, &audiospec, &rawaudio, &rawaudiolen);
  free(rawwave);
  if (res == NULL) return(-4);

  wav_cache[wav_cache_count].key = memgz;
  wav_cache[wav_cache_count].buf = rawaudio;
  wav_cache[wav_cache_count].len = rawaudiolen;
  wav_cache_count++;
  return(0);
}


static void assets_cache_free(void) {
  int i;
  for (i = 0; i < bmp_cache_count; i++) {
    if (bmp_cache[i].spr != NULL) {
      bmp_cache[i].spr->cached = 0;
      if (bmp_cache[i].spr->ptr != NULL) SDL_DestroyTexture(bmp_cache[i].spr->ptr);
      free(bmp_cache[i].spr);
      bmp_cache[i].spr = NULL;
    }
    bmp_cache[i].key = NULL;
  }
  bmp_cache_count = 0;
  for (i = 0; i < wav_cache_count; i++) {
    if (wav_cache[i].buf != NULL) SDL_FreeWAV(wav_cache[i].buf);
    wav_cache[i].buf = NULL;
    wav_cache[i].key = NULL;
    wav_cache[i].len = 0;
  }
  wav_cache_count = 0;
}


/* loads a gziped bmp image from memory into a SDL surface */
static SDL_Surface *loadgzbmp_surface(const unsigned char *memgz, long memgzlen) {
  unsigned char *rawimage;
  long rawimagelen;
  SDL_Surface *res, *resrgb;
  SDL_RWops *rwop;
  rawimage = ungz(memgz, memgzlen, &rawimagelen);
  if (rawimage == NULL) return(NULL);
  rwop = SDL_RWFromMem(rawimage, (int)rawimagelen);
  if (rwop == NULL) {
    free(rawimage);
    return(NULL);
  }
  res = SDL_LoadBMP_RW(rwop, 1);
  /* SDL_FreeRW(rwop); already freed by SDL_LoadBMP_RW() */
  free(rawimage);
  if (res == NULL) return(NULL);
  /* convert all surfaces to RGB 16-bit */
  resrgb = SDL_ConvertSurfaceFormat(res, SDL_PIXELFORMAT_RGB565, 0);
  SDL_FreeSurface(res);
  if (resrgb == NULL) return(NULL);
  /* set transparency color to 0xff0ff */
  SDL_SetColorKey(resrgb, SDL_TRUE, SDL_MapRGB(resrgb->format, 255, 0, 255));
  return(resrgb);
}


/* sanitized diagonal DPI for display 0 (fallback 96 when unknown/bogus) */
float gra_get_display_dpi(void) {
  float ddpi = 0.0f;
  if (SDL_GetDisplayDPI(0, &ddpi, NULL, NULL) != 0) return(96.0f);
  if ((ddpi < 72.0f) || (ddpi > 300.0f)) return(96.0f);
  return(ddpi);
}


/* suggest a default window size from the desktop display */
int gra_suggest_winsize(int *w, int *h) {
  SDL_Rect bounds;
  SDL_DisplayMode mode;
  float dpi;
  int sw, sh;
  int min_w, min_h, max_w, max_h;

  if (w == NULL || h == NULL) return(-1);

  dpi = gra_get_display_dpi();
  /* physical clamps: ~240x160 mm min, ~420x270 mm max (~ old 900x600 / 1600x1000 @ 96 DPI) */
  min_w = (int)(240.0f * dpi / 25.4f + 0.5f);
  min_h = (int)(160.0f * dpi / 25.4f + 0.5f);
  max_w = (int)(420.0f * dpi / 25.4f + 0.5f);
  max_h = (int)(270.0f * dpi / 25.4f + 0.5f);
  if (min_w < 640) min_w = 640;
  if (min_h < 480) min_h = 480;

  if (SDL_GetDisplayUsableBounds(0, &bounds) == 0) {
    sw = bounds.w;
    sh = bounds.h;
  } else if (SDL_GetDesktopDisplayMode(0, &mode) == 0) {
    sw = mode.w;
    sh = mode.h;
  } else {
    *w = min_w;
    *h = min_h;
    return(-1);
  }

  if (sw < 1 || sh < 1) {
    *w = min_w;
    *h = min_h;
    return(-1);
  }

  *w = sw * 90 / 100;
  *h = sh * 90 / 100;
  if (*w < min_w) *w = min_w;
  if (*h < min_h) *h = min_h;
  if (*w > max_w) *w = max_w;
  if (*h > max_h) *h = max_h;
  /* never exceed the usable desktop */
  if (*w > sw) *w = sw;
  if (*h > sh) *h = sh;
  return(0);
}


/* init the video subsystem */
int gra_init(int width, int height, int flags, const char *windowtitle, const void *titleicon, long titleicon_len) {
  Uint32 sdl_video_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE;
  SDL_Surface *titleiconsurface;

  if (flags & GRA_FULLSCREEN) sdl_video_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
  SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);

  if ((width <= 0) || (height <= 0)) {
    gra_suggest_winsize(&width, &height);
    printf("default window size: %dx%d (dpi=%.0f)\n", width, height, (double)gra_get_display_dpi());
  }

  window = SDL_CreateWindow(windowtitle, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, sdl_video_flags);

  renderer = SDL_CreateRenderer(window, -1, 0);
  if (renderer == NULL) {
    SDL_DestroyWindow(window);
    printf("Renderer could not be created! SDL_Error: %s\n", SDL_GetError());
    return(1);
  }

  /* set the window's titlebar icon */
  if ((titleicon != NULL) && (titleicon_len > 0)) {
    titleiconsurface = loadgzbmp_surface(titleicon, titleicon_len);
    if (titleiconsurface != NULL) {
      SDL_SetWindowIcon(window, titleiconsurface);
      SDL_FreeSurface(titleiconsurface);
    }
  }

  /* set the window's minimum size */
  SDL_SetWindowMinimumSize(window, 400, 300);

  /* init the ttf subsystem */
  if (TTF_Init() != 0) {
    printf("ERROR: TTF_Init() failed (%s)\n", TTF_GetError());
  }

  /* set up audio */
  {
    SDL_AudioSpec want;
    memset(&want, 0, sizeof(want));
    want.freq = 22050;
    want.format = AUDIO_S16;
    want.channels = 1;
    want.samples = 1024; /* more than this leads to a noticeable delay when playing sound effects */
    if (SDL_OpenAudio(&want, NULL) == 0) {
      SDL_PauseAudio(0); /* start audio */
    }
  }

  /* set up alpha blending (page shadow, translucent draws) */
  SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

  return(0);
}


/* decode wav.gz into the asset cache without playing (idempotent). */
void gra_preload_wavgz(const void *memgz, long memgzlen) {
  wav_cache_load(memgz, memgzlen);
}


/* plays wav.gz data (uses the decoded cache after the first load) */
void gra_playwavgz(const void *memgz, long memgzlen) {
  Uint8 *rawaudio = NULL;
  Uint32 rawaudiolen = 0;

  if (memgz == NULL) return;
  if (!wav_cache_find(memgz, &rawaudio, &rawaudiolen)) {
    if (wav_cache_load(memgz, memgzlen) != 0) return;
    if (!wav_cache_find(memgz, &rawaudio, &rawaudiolen)) return;
  }
  if ((rawaudio != NULL) && (rawaudiolen > 0)) {
    SDL_QueueAudio(1, rawaudio, rawaudiolen);
  }
}


/* close and clean up the graphic subsystem */
void gra_close(void) {
  assets_cache_free();
  if (renderer != NULL) {
    SDL_DestroyRenderer(renderer);
    renderer = NULL;
  }
  if (window != NULL) {
    SDL_DestroyWindow(window);
    window = NULL;
  }
  if (TTF_WasInit()) TTF_Quit();
  SDL_Quit();
}


void gra_clear(uint32_t rgb) {
  SDL_SetRenderDrawColor(renderer, RGB_R(rgb), RGB_G(rgb), RGB_B(rgb), 0xff);
  SDL_RenderClear(renderer);
}


void gra_bgtile(const void *memgz, long memgzlen) {
  static const void *cached_key = NULL;
  static struct gra_sprite *tile = NULL;
  static int tilw, tilh;
  int x, y;
  int winw, winh;

  /* decompress once -- this used to run on every frame and made panning crawl */
  if ((tile == NULL) || (cached_key != memgz)) {
    if (tile != NULL) gra_freesprite(tile);
    tile = gra_loadgzbmp(memgz, memgzlen);
    cached_key = memgz;
    if (tile == NULL) return;
    tilw = gra_getspritewidth(tile);
    tilh = gra_getspriteheight(tile);
    if ((tilw < 1) || (tilh < 1)) return;
  }

  gra_getwinsize(&winw, &winh);
  for (y = 0; y < winh; y += tilh) {
    for (x = 0; x < winw; x += tilw) {
      gra_drawsprite(tile, x, y);
    }
  }
}


void gra_switchfullscreen(void) {
  static int fullscreenflag = 0;
  fullscreenflag ^= 1;
  if (fullscreenflag != 0) {
      SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
    } else {
      SDL_SetWindowFullscreen(window, 0);
  }
  SDL_Delay(50); /* wait for 50ms - the video thread needs some time to set things up */
}


void gra_drawpartsprite(const struct gra_sprite *sprite, int srcx, int srcy, int srcwidth, int srcheight, int dstx, int dsty) {
  static SDL_Rect srcrect;
  static SDL_Rect dstrect;
  if (sprite == NULL) return;
  dstrect.x = dstx * SCALE;
  dstrect.y = dsty * SCALE;
  dstrect.w = srcwidth * SCALE;
  dstrect.h = srcheight * SCALE;
  srcrect.x = srcx;
  srcrect.y = srcy;
  srcrect.w = srcwidth;
  srcrect.h = srcheight;
  SDL_RenderCopy(renderer, sprite->ptr, &srcrect, &dstrect);
}


void gra_drawsprite(const struct gra_sprite *sprite, int x, int y) {
  static SDL_Rect rect;
  if (sprite == NULL) return;
  rect.x = x * SCALE;
  rect.y = y * SCALE;
  rect.w = sprite->w * SCALE;
  rect.h = sprite->h * SCALE;
  SDL_RenderCopy(renderer, sprite->ptr, NULL, &rect);
}


void gra_refresh(void) {
  SDL_RenderPresent(renderer);
}


/* loads a gziped bmp image from memory and returns a gra_sprite.
 * Embedded blobs are decoded once and kept in a permanent cache. */
struct gra_sprite *gra_loadgzbmp(const void *memgz, long memgzlen) {
  SDL_Surface *surface;
  struct gra_sprite *res;

  if (memgz == NULL) return(NULL);
  res = bmp_cache_find(memgz);
  if (res != NULL) return(res);

  surface = loadgzbmp_surface(memgz, memgzlen);
  if (surface == NULL) return(NULL);
  res = malloc(sizeof(struct gra_sprite));
  if (res == NULL) {
    SDL_FreeSurface(surface);
    return(NULL);
  }
  res->cached = 0;
  res->ptr = SDL_CreateTextureFromSurface(renderer, surface);
  SDL_FreeSurface(surface);
  if (res->ptr == NULL) {
    free(res);
    return(NULL);
  }
  SDL_QueryTexture(res->ptr, NULL, NULL, &(res->w), &(res->h));
  bmp_cache_add(memgz, res);
  return(res);
}


void loadSpriteSheet(struct gra_sprite **sprites, int width, int height, int itemcount, const void *memptr, int memlen) {
  SDL_Surface *spritesheet, *item;
  SDL_Rect rect;
  int i;
  spritesheet = loadgzbmp_surface(memptr, memlen);
  if (spritesheet == NULL) puts("bmp is NULL!!!");
  item = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 32, 0xFF000000L, 0x00FF0000L, 0x0000FF00L, 0x000000FFL);
  if (item == NULL) puts("item is NULL!!!");
  for (i = 0; i < itemcount; i++) {
    rect.x = i * width;
    rect.y = 0;
    rect.w = width;
    rect.h = height;
    SDL_FillRect(item, NULL, 0);
    SDL_BlitSurface(spritesheet, &rect, item, NULL);
    sprites[i] = malloc(sizeof(struct gra_sprite));
    sprites[i]->cached = 0;
    sprites[i]->ptr = SDL_CreateTextureFromSurface(renderer, item);
    if (sprites[i]->ptr == NULL) puts("sprites[i]->ptr is NULL!!!");
    SDL_QueryTexture(sprites[i]->ptr, NULL, NULL, &(sprites[i]->w), &(sprites[i]->h));
  }
  SDL_FreeSurface(item);
  SDL_FreeSurface(spritesheet);
}


int gra_getspritewidth(const struct gra_sprite *sprite) {
  if (sprite == NULL) return(0);
  return(sprite->w);
}


int gra_getspriteheight(const struct gra_sprite *sprite) {
  if (sprite == NULL) return(0);
  return(sprite->h);
}


int gra_drawrect(int x, int y, int width, int height, uint32_t rgb, uint8_t a, int fillflag) {
  SDL_Rect rect;
  int res;
  rect.x = x;
  rect.y = y;
  rect.w = width;
  rect.h = height;
  res = SDL_SetRenderDrawColor(renderer, RGB_R(rgb), RGB_G(rgb), RGB_B(rgb), a);
  if (res != 0) return(-1);
  if (fillflag == 0) {
    res = SDL_RenderDrawRect(renderer, &rect);
  } else {
    res = SDL_RenderFillRect(renderer, &rect);
  }
  if (res != 0) return(-2);
  return(0);
}


void gra_roundrect(int x, int y, int width, int height, int rad, uint32_t rgb, uint8_t a, int fillflag) {
  int x0, y0, x1, y1;
  int px, py, err;
  int ctlx, ctly, ctrx, ctry, cblx, cbly, cbrx, cbry;

  (void)fillflag; /* outline only */

  if ((width < 2) || (height < 2)) return;
  if (rad < 1) {
    gra_drawrect(x, y, width, height, rgb, a, 0);
    return;
  }
  if (rad * 2 > width) rad = width / 2;
  if (rad * 2 > height) rad = height / 2;

  x0 = x;
  y0 = y;
  x1 = x + width - 1;
  y1 = y + height - 1;

  SDL_SetRenderDrawColor(renderer, RGB_R(rgb), RGB_G(rgb), RGB_B(rgb), a);

  /* straight sides between the rounded corners */
  SDL_RenderDrawLine(renderer, x0 + rad, y0, x1 - rad, y0);
  SDL_RenderDrawLine(renderer, x0 + rad, y1, x1 - rad, y1);
  SDL_RenderDrawLine(renderer, x0, y0 + rad, x0, y1 - rad);
  SDL_RenderDrawLine(renderer, x1, y0 + rad, x1, y1 - rad);

  /* quarter-circle corners (Bresenham) */
  ctlx = x0 + rad; ctly = y0 + rad;
  ctrx = x1 - rad; ctry = y0 + rad;
  cblx = x0 + rad; cbly = y1 - rad;
  cbrx = x1 - rad; cbry = y1 - rad;
  px = rad;
  py = 0;
  err = 1 - px;
  while (px >= py) {
    SDL_RenderDrawPoint(renderer, ctlx - px, ctly - py);
    SDL_RenderDrawPoint(renderer, ctlx - py, ctly - px);
    SDL_RenderDrawPoint(renderer, ctrx + px, ctry - py);
    SDL_RenderDrawPoint(renderer, ctrx + py, ctry - px);
    SDL_RenderDrawPoint(renderer, cblx - px, cbly + py);
    SDL_RenderDrawPoint(renderer, cblx - py, cbly + px);
    SDL_RenderDrawPoint(renderer, cbrx + px, cbry + py);
    SDL_RenderDrawPoint(renderer, cbrx + py, cbry + px);
    py++;
    if (err < 0) {
      err += 2 * py + 1;
    } else {
      px--;
      err += 2 * (py - px) + 1;
    }
  }
}


void gra_drawline(int x1, int y1, int x2, int y2, int thickness, uint32_t rgb, uint8_t a) {
  int t;
  SDL_Rect rect;
  /* set drawing color */
  SDL_SetRenderDrawColor(renderer, RGB_R(rgb), RGB_G(rgb), RGB_B(rgb), a);
  /* for 1px lines rely on SDL */
  if (thickness == 1) {
    SDL_RenderDrawLine(renderer, x1, y1, x2, y2);
    return;
  }
  /* make sure that the line goes from left to right (later code relies on that assumption, also SDL on some platforms is troubled by rects with a negative width */
  if (x1 > x2) {
    t = x1;
    x1 = x2;
    x2 = t;
    t = y1;
    y1 = y2;
    y2 = t;
  }
  /* optimize horizontal and vertical lines by drawing rectangles (much faster) */
  if (x1 == x2) {
    rect.x = x1 - (thickness >> 1);
    rect.y = y1;
    rect.w = thickness;
    rect.h = y2 - y1;
    SDL_RenderFillRect(renderer, &rect);
  } else if (y1 == y2) {
    rect.x = x1;
    rect.y = y1 - (thickness >> 1);
    rect.w = x2 - x1;
    rect.h = thickness;
    SDL_RenderFillRect(renderer, &rect);
  } else { /* diagonal line (only 45 degrees) */
    /* quick and dirty, only 45 degrees are possible - prints out many rectangles */
    int yy = y1;
    for (; x1 < x2; x1++) {
      gra_drawrect(x1 - (thickness >> 1), yy - (thickness >> 1), thickness, thickness, rgb, a, 1);
      if (y1 < y2) {
        yy++;
      } else {
        yy--;
      }
    }
  }
}


void gra_circle_filled(int cx, int cy, int rad, uint32_t rgb, uint8_t a) {
  int y, bound;
  double ro, ro_out, ro_in;
  Uint8 cr = RGB_R(rgb), cg = RGB_G(rgb), cb = RGB_B(rgb);

  if (rad < 1 || a == 0) return;
  ro = (double)rad;
  ro_out = ro + 0.5;
  ro_in = (ro > 0.5) ? (ro - 0.5) : 0.0;
  bound = rad + 1;

  for (y = -bound; y <= bound; y++) {
    double y2 = (double)(y * y);
    double xs_out2, xs_in2;
    int x, x_out, x_in;

    if (y2 > ro_out * ro_out) continue;
    xs_out2 = ro_out * ro_out - y2;
    x_out = (int)ceil(sqrt(xs_out2));

    if (y2 <= ro_in * ro_in) {
      xs_in2 = ro_in * ro_in - y2;
      x_in = (int)floor(sqrt(xs_in2));
    } else {
      x_in = -1; /* no fully-covered run on this row */
    }

    /* soft left edge */
    for (x = -x_out; x <= -x_in - 1; x++) {
      double d = sqrt((double)(x * x) + y2);
      double cov = (ro_out) - d;
      Uint8 pa;
      if (cov <= 0.0) continue;
      if (cov > 1.0) cov = 1.0;
      pa = (Uint8)((double)a * cov + 0.5);
      if (pa == 0) continue;
      SDL_SetRenderDrawColor(renderer, cr, cg, cb, pa);
      SDL_RenderDrawPoint(renderer, cx + x, cy + y);
    }
    /* solid interior span */
    if (x_in >= 0) {
      SDL_SetRenderDrawColor(renderer, cr, cg, cb, a);
      gra_drawrect(cx - x_in, cy + y, x_in * 2 + 1, 1, rgb, a, 1);
    }
    /* soft right edge */
    for (x = x_in + 1; x <= x_out; x++) {
      double d = sqrt((double)(x * x) + y2);
      double cov = (ro_out) - d;
      Uint8 pa;
      if (cov <= 0.0) continue;
      if (cov > 1.0) cov = 1.0;
      pa = (Uint8)((double)a * cov + 0.5);
      if (pa == 0) continue;
      SDL_SetRenderDrawColor(renderer, cr, cg, cb, pa);
      SDL_RenderDrawPoint(renderer, cx + x, cy + y);
    }
  }
}


void gra_circle(int x0, int y0, int rad, uint32_t rgb, uint8_t a) {
  /* SDL_RenderDrawPoint(renderer, x, y); */
  int x = rad;
  int y = 0;
  int radiusError = 1 - x;

  /* set the 'drawing color' first */
  SDL_SetRenderDrawColor(renderer, RGB_R(rgb), RGB_G(rgb), RGB_B(rgb), a);

  /* draw the outer circle */
  while(x >= y) {
    SDL_RenderDrawPoint(renderer, x + x0, y + y0);
    SDL_RenderDrawPoint(renderer, y + x0,  x + y0);
    SDL_RenderDrawPoint(renderer, -x + x0,  y + y0);
    SDL_RenderDrawPoint(renderer, -y + x0,  x + y0);
    SDL_RenderDrawPoint(renderer, -x + x0, -y + y0);
    SDL_RenderDrawPoint(renderer, -y + x0, -x + y0);
    SDL_RenderDrawPoint(renderer, x + x0, -y + y0);
    SDL_RenderDrawPoint(renderer, y + x0, -x + y0);
    y++;
    if (radiusError < 0) {
      radiusError += 2 * y + 1;
    } else {
      x--;
      radiusError += 2 * (y - x) + 1;
    }
  }

}


/* Soft-edged filled annulus. Looks like a pen stroke, not stacked Bresenham dots. */
void gra_ring(int cx, int cy, int rad_outer, int thickness, uint32_t rgb, uint8_t a) {
  int rad_inner;
  int bound;
  int x, y;
  double ro, ri;
  Uint8 r = RGB_R(rgb), g = RGB_G(rgb), b = RGB_B(rgb);

  if (rad_outer < 1 || thickness < 1 || a == 0) return;
  if (thickness > rad_outer) thickness = rad_outer;
  rad_inner = rad_outer - thickness;
  ro = (double)rad_outer;
  ri = (double)rad_inner;
  bound = rad_outer + 1;

  for (y = -bound; y <= bound; y++) {
    for (x = -bound; x <= bound; x++) {
      double d = sqrt((double)(x * x + y * y));
      double cov = 1.0;
      Uint8 pa;

      if (d >= ro + 0.5 || d <= ri - 0.5) continue;
      /* 1px soft falloff on the inner and outer edges */
      if (d > ro - 0.5) cov = (ro + 0.5) - d;
      else if (d < ri + 0.5) cov = d - (ri - 0.5);
      if (cov <= 0.0) continue;
      if (cov > 1.0) cov = 1.0;
      pa = (Uint8)((double)a * cov + 0.5);
      if (pa == 0) continue;
      SDL_SetRenderDrawColor(renderer, r, g, b, pa);
      SDL_RenderDrawPoint(renderer, cx + x, cy + y);
    }
  }
}


struct gra_sprite *gra_text2sprite(const char *message, const void *font_ttf, long font_ttf_len, int fontSize, uint32_t fg, int32_t bg, uint8_t a, int maxwidth) {
  SDL_Surface *surface;
  SDL_Color txtcolor;
  SDL_RWops *rwop;
  TTF_Font *font = NULL;
  struct gra_sprite *res;

  /* if no max width, then set maxwidth to width of the window */
  if (maxwidth < 1) gra_getwinsize(&maxwidth, NULL);

  /* Load the font */
  rwop = SDL_RWFromConstMem(font_ttf, (int)font_ttf_len);
  if (rwop == NULL) {
    puts("OOOPS rwop is NULL!!!!");
    return(NULL);
  }
  font = TTF_OpenFontRW(rwop, 1, fontSize);
  txtcolor.r = RGB_R(fg);
  txtcolor.g = RGB_G(fg);
  txtcolor.b = RGB_B(fg);
  txtcolor.a = 0xff; /* alpha here doesn't work anyway, I set it later on the texture */
  surface = TTF_RenderUTF8_Blended_Wrapped(font, message, txtcolor, (Uint32)maxwidth);
  if (bg >= 0) {
    SDL_Surface *bgsurface;
    bgsurface = SDL_ConvertSurface(surface, surface->format, SDL_SWSURFACE);
    SDL_FillRect(bgsurface, NULL, SDL_MapRGB(bgsurface->format, RGB_R(bg), RGB_G(bg), RGB_B(bg)));
    SDL_BlitSurface(surface, NULL, bgsurface, NULL);
    SDL_FreeSurface(surface);
    surface = bgsurface;
    bgsurface = NULL;
  }
  res = malloc(sizeof(struct gra_sprite));
  res->cached = 0;
  res->ptr = SDL_CreateTextureFromSurface(renderer, surface);
  SDL_FreeSurface(surface);
  SDL_QueryTexture(res->ptr, NULL, NULL, &(res->w), &(res->h));
  SDL_SetTextureAlphaMod(res->ptr, a);
  TTF_CloseFont(font);
  /* SDL_FreeRW(rwop); Don't try to free the rwop, it's already free! */
  return(res);
}





static int utf8_char_bytes(unsigned char c) {
  if (c < 0x80u) return(1);
  if ((c & 0xe0u) == 0xc0u) return(2);
  if ((c & 0xf0u) == 0xe0u) return(3);
  if ((c & 0xf8u) == 0xf0u) return(4);
  return(1);
}


struct gra_sprite *gra_shadowtext2sprite(const char *message, const void *font_ttf, long font_ttf_len, int fontSize, uint32_t fg, uint8_t a, int tracking) {
  SDL_RWops *rwop;
  TTF_Font *font;
  SDL_Color txtcolor, shcolor;
  SDL_Surface *canvas = NULL;
  struct gra_sprite *res = NULL;
  const char *p;
  const int shadow = 4;
  int n = 0, i, total_w = 0, glyph_h = 0, x;
  SDL_Surface **glyphs = NULL;
  SDL_Surface **shadows = NULL;

  if ((message == NULL) || (message[0] == '\0')) return(NULL);
  if (tracking < 0) tracking = 0;

  rwop = SDL_RWFromConstMem(font_ttf, (int)font_ttf_len);
  if (rwop == NULL) return(NULL);
  font = TTF_OpenFontRW(rwop, 1, fontSize);
  if (font == NULL) return(NULL);

  txtcolor.r = RGB_R(fg);
  txtcolor.g = RGB_G(fg);
  txtcolor.b = RGB_B(fg);
  txtcolor.a = 0xff;
  shcolor.r = shcolor.g = shcolor.b = 0;
  shcolor.a = 0xff;

  for (p = message; *p != '\0'; p += utf8_char_bytes((unsigned char)*p)) n++;
  glyphs = (SDL_Surface **)calloc((size_t)n, sizeof(*glyphs));
  shadows = (SDL_Surface **)calloc((size_t)n, sizeof(*shadows));
  if ((glyphs == NULL) || (shadows == NULL)) {
    free(glyphs);
    free(shadows);
    TTF_CloseFont(font);
    return(NULL);
  }

  i = 0;
  for (p = message; *p != '\0'; ) {
    char ch[8];
    int len = utf8_char_bytes((unsigned char)*p);
    if (len > 7) len = 7;
    memcpy(ch, p, (size_t)len);
    ch[len] = '\0';
    p += len;
    glyphs[i] = TTF_RenderUTF8_Blended(font, ch, txtcolor);
    shadows[i] = TTF_RenderUTF8_Blended(font, ch, shcolor);
    if (glyphs[i] != NULL) {
      total_w += glyphs[i]->w;
      if (glyphs[i]->h > glyph_h) glyph_h = glyphs[i]->h;
    }
    i++;
  }
  TTF_CloseFont(font);

  if (n > 1) total_w += tracking * (n - 1);
  if ((total_w < 1) || (glyph_h < 1)) goto cleanup;

  canvas = SDL_CreateRGBSurfaceWithFormat(0, total_w + shadow, glyph_h + shadow, 32, SDL_PIXELFORMAT_ARGB8888);
  if (canvas == NULL) goto cleanup;
  SDL_FillRect(canvas, NULL, 0);
  SDL_SetSurfaceBlendMode(canvas, SDL_BLENDMODE_BLEND);

  /* shadow pass */
  x = 0;
  for (i = 0; i < n; i++) {
    SDL_Rect dst;
    if (shadows[i] == NULL) {
      if (glyphs[i] != NULL) x += glyphs[i]->w + tracking;
      continue;
    }
    dst.x = x + shadow - 1;
    dst.y = shadow - 1;
    dst.w = shadows[i]->w;
    dst.h = shadows[i]->h;
    SDL_SetSurfaceAlphaMod(shadows[i], 110);
    SDL_SetSurfaceBlendMode(shadows[i], SDL_BLENDMODE_BLEND);
    SDL_BlitSurface(shadows[i], NULL, canvas, &dst);
    x += (glyphs[i] != NULL ? glyphs[i]->w : shadows[i]->w) + tracking;
  }

  /* foreground pass */
  x = 0;
  for (i = 0; i < n; i++) {
    SDL_Rect dst;
    if (glyphs[i] == NULL) continue;
    dst.x = x;
    dst.y = 0;
    dst.w = glyphs[i]->w;
    dst.h = glyphs[i]->h;
    SDL_SetSurfaceBlendMode(glyphs[i], SDL_BLENDMODE_BLEND);
    SDL_BlitSurface(glyphs[i], NULL, canvas, &dst);
    x += glyphs[i]->w + tracking;
  }

  res = malloc(sizeof(struct gra_sprite));
  if (res != NULL) {
    res->cached = 0;
    res->ptr = SDL_CreateTextureFromSurface(renderer, canvas);
    SDL_QueryTexture(res->ptr, NULL, NULL, &(res->w), &(res->h));
    SDL_SetTextureAlphaMod(res->ptr, a);
  }

cleanup:
  if (canvas != NULL) SDL_FreeSurface(canvas);
  if (glyphs != NULL) {
    for (i = 0; i < n; i++) {
      if (glyphs[i] != NULL) SDL_FreeSurface(glyphs[i]);
    }
    free(glyphs);
  }
  if (shadows != NULL) {
    for (i = 0; i < n; i++) {
      if (shadows[i] != NULL) SDL_FreeSurface(shadows[i]);
    }
    free(shadows);
  }
  return(res);
}


void gra_freesprite(struct gra_sprite *spr) {
  if (spr == NULL) return;
  /* Cached media sprites live until gra_close(); menu autofree must not destroy them. */
  if (spr->cached) return;
  if (spr->ptr != NULL) SDL_DestroyTexture(spr->ptr);
  free(spr);
}


void gra_getwinsize(int *w, int *h) {
  SDL_GetWindowSize(window, w, h);
}
