#include <math.h>
#include <stdatomic.h>
#include "internal.h"
#include "fbuf.h"

#define RGBSIZE 3

// number of worker threads
// FIXME fit to local machine, but more than 3 never seems to help
#define POPULATION 3

// a worker can have up to three qstates enqueued for work
#define WORKERDEPTH 3

// this palette entry is a sentinel for a transparent pixel (and thus caps
// the palette at 65535 other entries).
#define TRANS_PALETTE_ENTRY 65535

// bytes per element in the auxiliary vector
#define AUXVECELEMSIZE 2

// three scaled sixel [0..100x3] components plus a population count.
typedef struct qsample {
  unsigned char comps[RGBSIZE];
  uint32_t pop;
} qsample;

// lowest samples for each node. first-order nodes track 1000 points in
// sixelspace (10x10x10). there are eight possible second-order nodes from a
// fractured first-order node, covering 125 points each (5x5x5).
typedef struct qnode {
  qsample q;
  // cidx plays two roles. during merge, we select the active set, and extract
  // them (since they'll be sorted, we can't operate directly on the octree).
  // here, we use cidx to map back to the initial octree entry, as we need
  // update them (from the active set) at the end of merging. afterwards, the
  // high bit indicates that it was chosen, and the cidx is a valid index into
  // the final color table. it is otherwise a link to the merged qnode.
  // during initial filtering, qlink determines whether a node has fractured:
  // if qlink is non-zero, it is a one-biased index to an onode.
  // FIXME combine these once more, but for now to keep it easy, we have two.
  // qlink links back into the octree.
  uint16_t qlink;
  uint16_t cidx;
} qnode;

// an octree-style node, used for fractured first-order nodes. the first
// bit is whether we're on the top or bottom of the R, then G, then B.
typedef struct onode {
  qnode* q[8];
} onode;

// we set P2 based on whether there is any transparency in the sixel. if not,
// use SIXEL_P2_ALLOPAQUE (0), for faster drawing in certain terminals.
typedef enum {
  SIXEL_P2_ALLOPAQUE = 0,
  SIXEL_P2_TRANS = 1,
} sixel_p2_e;

// data for a single sixelband. a vector of sixel rows, one for each color
// represented within the band. we initially create a vector for every
// possible (quantized) color, and then collapse it.
typedef struct sixelband {
  int size;     // capacity FIXME if same for all, eliminate this
  char** vecs;  // array of vectors, many of which can be NULL
} sixelband;

// across the life of the sixel, we'll need to wipe and restore cells, without
// recourse to the original RGBA data. this is prohibitively expensive to do on
// the encoded data, since it might require expanding or collapsing sections in
// the middle (we could use a rope, but it would be annoying). instead, we keep
// for each sixelrow (i.e. for every 6 rows) a vector of colors and distinct
// encoded sections (i.e. *not* from some common long single allocation). this
// way, the encoded sections can be easily and cheaply changed (since they're
// small, and quickly indexed by sixelrow * color)). whenever we want to emit
// the sixel, we just gather all these dynamic sections and write them
// successively into the fbuf. this table can be built up in parallel, since
// it's isolated among sixelrows -- the sixelrow is then the natural work unit.
// this sixelmap is kept across the life of the sprixel; any longlived state
// must be here, whereas state necessary only for rendering ought be in qstate.
typedef struct sixelmap {
  int colors;
  int sixelbands;
  sixelband* bands;      // |sixelbands| collections of sixel vectors
  sixel_p2_e p2;         // set to SIXEL_P2_TRANS if we have transparent pixels
} sixelmap;

typedef struct qstate {
  int refcount; // initialized to worker count
  atomic_int bandbuilder; // threads take bands as their work unit
  // we always work in terms of quantized colors (quantization is the first
  // step of rendering), using indexes into the derived palette. the actual
  // palette need only be stored during the initial render, since the sixel
  // header can be preserved, and the palette is unchanged by wipes/restores.
  unsigned char* table;  // |colors| x RGBSIZE components
  qnode* qnodes;
  onode* onodes;
  unsigned dynnodes_free;
  unsigned dynnodes_total;
  unsigned onodes_free;
  unsigned onodes_total;
  const struct blitterargs* bargs;
  const uint32_t* data;
  int linesize;
  sixelmap* smap;
  // these are the leny and lenx passed to sixel_blit(), which are likely
  // different from those reachable through bargs->len{y,x}!
  int leny, lenx;
} qstate;

// a work_queue per worker thread. if used == WORKERDEPTH, this thread is
// backed up, and we cannot enqueue to it. writeto wraps around the array.
typedef struct work_queue {
  qstate* qstates[WORKERDEPTH];
  unsigned writeto;
  unsigned used;
  struct sixel_engine* sengine;
} work_queue;

// we keep a few worker threads (POPULATION) spun up to assist with
// quantization. each has an array of up to WORKERDEPTH qstates to work on.
typedef struct sixel_engine {
  pthread_mutex_t lock;
  pthread_cond_t cond;
  work_queue queues[POPULATION];
  pthread_t tids[POPULATION];
  bool done;
} sixel_engine;

// enqueue |qs| to any workers with available space. the number of workers with
// a reference will be stored in |qs|->refcount.
static void
enqueue_to_workers(sixel_engine* eng, qstate* qs){
  if(eng == NULL){
    return;
  }
  int usecount = 0;
  pthread_mutex_lock(&eng->lock);
  for(int i = 0 ; i < POPULATION ; ++i){
    work_queue* wq = &eng->queues[i];
    if(wq->used < WORKERDEPTH){
      wq->qstates[wq->writeto] = qs;
      ++wq->used;
      ++usecount;
      if(++wq->writeto == WORKERDEPTH){
        wq->writeto = 0;
      }
    }
  }
  qs->refcount = usecount;
  pthread_mutex_unlock(&eng->lock);
  if(usecount){
    pthread_cond_broadcast(&eng->cond);
  }
}

// block until all workers have finished up with |qs|
static void
block_on_workers(sixel_engine* eng, qstate* qs){
  if(eng == NULL){
    return;
  }
  pthread_mutex_lock(&eng->lock);
  while(qs->refcount){
    pthread_cond_wait(&eng->cond, &eng->lock);
  }
  pthread_mutex_unlock(&eng->lock);
}

// returns the number of individual sixels necessary to represent the specified
// pixel geometry. these might encompass more pixel rows than |dimy| would
// suggest, up to the next multiple of 6 (i.e. a single row becomes a 6-row
// bitmap; as do two, three, four, five, or six rows). input is scaled geometry.
static inline int
sixelcount(int dimy, int dimx){
  return (dimy + 5) / 6 * dimx;
}

// returns the number of sixel bands (horizontal series of sixels, aka 6 rows)
// for |dimy| source rows. sixels are encoded as a series of sixel bands.
static inline int
sixelbandcount(int dimy){
  return sixelcount(dimy, 1);
}

// whip up a sixelmap sans data for the specified pixel geometry and color
// register count.
static sixelmap*
sixelmap_create(int dimy){
  sixelmap* ret = malloc(sizeof(*ret));
  if(ret){
    ret->p2 = SIXEL_P2_ALLOPAQUE;
    // they'll be initialized by their workers, possibly in parallel
    ret->sixelbands = sixelbandcount(dimy);
    ret->bands = malloc(sizeof(*ret->bands) * ret->sixelbands);
    if(ret->bands == NULL){
      free(ret);
      return NULL;
    }
    for(int i = 0 ; i < ret->sixelbands ; ++i){
      ret->bands[i].size = 0;
    }
    ret->colors = 0;
  }
  return ret;
}

static inline void
sixelband_free(sixelband* s){
  for(int j = 0 ; j < s->size ; ++j){
    free(s->vecs[j]);
  }
  free(s->vecs);
}

void sixelmap_free(sixelmap *s){
  if(s){
    for(int i = 0 ; i < s->sixelbands ; ++i){
      sixelband_free(&s->bands[i]);
    }
    free(s->bands);
    free(s);
  }
}

// convert rgb [0..255] to sixel [0..99]
static inline unsigned
ss(unsigned c){
  unsigned r = round(c * 100.0 / 255); // use real [0..100] scaling
  return r > 99 ? 99: r;
}

// get the keys for an rgb point. the returned value is on [0..999], and maps
// to a static qnode. the second value is on [0..7], and maps within the
// fractured onode (if necessary).
static inline unsigned
qnode_keys(unsigned r, unsigned g, unsigned b, unsigned *skey){
  unsigned ssr = ss(r);
  unsigned ssg = ss(g);
  unsigned ssb = ss(b);
  unsigned ret = ssr / 10 * 100 + ssg / 10 * 10 + ssb / 10;
  *skey = (((ssr % 10) / 5) << 2u) +
          (((ssg % 10) / 5) << 1u) +
          ((ssb % 10) / 5);
//fprintf(stderr, "0x%02x 0x%02x 0x%02x %02u %02u %02u %u %u\n", r, g, b, ssr, ssg, ssb, ret, *skey);
  return ret;
}

// have we been chosen for the color table?
static inline bool
chosen_p(const qnode* q){
  return q->cidx & 0x8000u;
}

static inline unsigned
make_chosen(unsigned cidx){
  return cidx | 0x8000u;
}

// get the cidx without the chosen bit
static inline unsigned
qidx(const qnode* q){
  return q->cidx & ~0x8000u;
}

#define QNODECOUNT 1000

// create+zorch an array of QNODECOUNT qnodes. this is 1000 entries covering
// 1000 sixel colors each (we pretend 100 doesn't exist, working on [0..99],
// heh). in addition, at the end we allocate |colorregs| qnodes, to be used
// dynamically in "fracturing". the original QNODECOUNT nodes are a static
// octree, flattened into an array; the latter are used as an actual octree.
// we must have 8 dynnodes available for every onode we create, or we can run
// into a situation where we don't have an available dynnode
// (see insert_color()).
static qstate*
alloc_qstate(unsigned colorregs){
  qstate* qs = malloc(sizeof(*qs));
  if(qs){
    qs->dynnodes_free = colorregs;
    qs->dynnodes_total = qs->dynnodes_free;
    if((qs->qnodes = malloc((QNODECOUNT + qs->dynnodes_total) * sizeof(qnode))) == NULL){
      free(qs);
      return NULL;
    }
    qs->onodes_free = qs->dynnodes_total / 8;
    qs->onodes_total = qs->onodes_free;
    if((qs->onodes = malloc(qs->onodes_total * sizeof(*qs->onodes))) == NULL){
      free(qs->qnodes);
      free(qs);
      return NULL;
    }
    // don't technically need to clear the components, as we could
    // check the pop, but it's hidden under the compulsory cache misses.
    // we only initialize the static nodes, not the dynamic ones--we know
    // when we pull a dynamic one that it needs its popcount initialized.
    memset(qs->qnodes, 0, sizeof(qnode) * QNODECOUNT);
    qs->table = NULL;
  }
  return qs;
}

// free internals of qstate object
static void
free_qstate(qstate *qs){
  if(qs){
    loginfo("freeing qstate");
    free(qs->qnodes);
    free(qs->onodes);
    free(qs->table);
    free(qs);
  }
}

// insert a color from the source image into the octree.
static inline int
insert_color(qstate* qs, uint32_t pixel){
  const unsigned r = ncpixel_r(pixel);
  const unsigned g = ncpixel_g(pixel);
  const unsigned b = ncpixel_b(pixel);
  unsigned skey;
  const unsigned key = qnode_keys(r, g, b, &skey);
  assert(key < QNODECOUNT);
  assert(skey < 8);
  qnode* q = &qs->qnodes[key];
  if(q->q.pop == 0 && q->qlink == 0){ // previously-unused node
    q->q.comps[0] = r;
    q->q.comps[1] = g;
    q->q.comps[2] = b;
    q->q.pop = 1;
    ++qs->smap->colors;
    return 0;
  }
  onode* o;
  // it's not a fractured node, but it's been used. check to see if we
  // match the secondary key of what's here.
  if(q->qlink == 0){
    unsigned skeynat;
    qnode_keys(q->q.comps[0], q->q.comps[1], q->q.comps[2], &skeynat);
    if(skey == skeynat){
      ++q->q.pop; // pretty good match
      return 0;
    }
    // we want to fracture. if we have no onodes, though, we can't.
    // we also need at least one dynamic qnode. note that this means we might
    // open an onode just to fail to insert our current lookup; that's fine;
    // it's a symmetry between creation and extension.
    if(qs->dynnodes_free == 0 || qs->onodes_free == 0){
//fprintf(stderr, "NO FREE ONES %u\n", key);
      ++q->q.pop; // not a great match, but we're already scattered
      return 0;
    }
    // get the next free onode and zorch it out
    o = qs->onodes + qs->onodes_total - qs->onodes_free;
//fprintf(stderr, "o: %p obase: %p %u\n", o, qs->onodes, qs->onodes_total - qs->onodes_free);
    memset(o, 0, sizeof(*o));
    // get the next free dynnode and assign it to o, account for dnode
    o->q[skeynat] = &qs->qnodes[QNODECOUNT + qs->dynnodes_total - qs->dynnodes_free];
    --qs->dynnodes_free;
    // copy over our own details
    memcpy(o->q[skeynat], q, sizeof(*q));
    // set qlink to one-biased index of the onode, and account for onode
    q->qlink = qs->onodes_total - qs->onodes_free + 1;
    --qs->onodes_free;
    // reset our own population count
    q->q.pop = 0;
  }else{
    // the node has already been fractured
    o = qs->onodes + (q->qlink - 1);
  }
  if(o->q[skey]){
    // our subnode is already present, huzzah. increase its popcount.
    ++o->q[skey]->q.pop;
    return 0;
  }
  // we try otherwise to insert ourselves into o. this requires a free dynnode.
  if(qs->dynnodes_free == 0){
//fprintf(stderr, "NO DYNFREE %u\n", key);
    // this should never happen, because we always ought have 8 dynnodes for
    // every possible onode.
    return -1;
  }
  // get the next free dynnode and assign it to o, account for dnode
  o->q[skey] = &qs->qnodes[QNODECOUNT + qs->dynnodes_total - qs->dynnodes_free];
  --qs->dynnodes_free;
  o->q[skey]->q.pop = 1;
  o->q[skey]->q.comps[0] = r;
  o->q[skey]->q.comps[1] = g;
  o->q[skey]->q.comps[2] = b;
  o->q[skey]->qlink = 0;
  o->q[skey]->cidx = 0;
  ++qs->smap->colors;
//fprintf(stderr, "INSERTED[%u]: %u %u %u\n", key, q->q.comps[0], q->q.comps[1], q->q.comps[2]);
  return 0;
}

// resolve the input color to a color table index following any postprocessing
// of the octree.
static inline int
find_color(const qstate* qs, uint32_t pixel){
  const unsigned r = ncpixel_r(pixel);
  const unsigned g = ncpixel_g(pixel);
  const unsigned b = ncpixel_b(pixel);
  unsigned skey;
  const unsigned key = qnode_keys(r, g, b, &skey);
  const qnode* q = &qs->qnodes[key];
  if(q->qlink && q->q.pop == 0){
    if(qs->onodes[q->qlink - 1].q[skey]){
      q = qs->onodes[q->qlink - 1].q[skey];
    }else{
      logpanic("internal error: no color for 0x%016x", pixel);
      return -1;
    }
  }
  return qidx(q);
}

// the P2 parameter on a sixel specifies how unspecified pixels are drawn.
// if P2 is 1, unspecified pixels are transparent. otherwise, they're drawn
// as something else. some terminals (e.g. foot) can draw more quickly if
// P2 is 0, so we set that when we have no transparent pixels -- i.e. when
// all TAM entries are 0. P2 is at a fixed location in the sixel header.
// obviously, the sixel must already exist.
static inline void
change_p2(char* sixel, sixel_p2_e value){
  sixel[4] = value + '0';
}

static inline void
write_rle(char* vec, int* voff, int rle, int rep){
  if(rle > 2){
    *voff += sprintf(vec + *voff, "!%d", rle);
  }else if(rle == 2){
    vec[(*voff)++] = rep;
  }
  if(rle){
    vec[(*voff)++] = rep;
  }
  vec[*voff] = '\0';
}

// one for each color in the band we're building. |rle| tracks the number of
// consecutive unwritten instances of the current non-0 rep, which is itself
// tracked in |rep|. |wrote| tracks the number of sixels written out for this
// color. whenever we get a new rep (this only happens for non-zero reps),
// we must write any old rle rep, plus any zero-reps since then.
struct band_extender {
  int length; // current length of the vector
  int rle;    // current rep count of non-zero sixel for this color
  int wrote;  // number of sixels we've written out
  int rep;    // representation, 0..63
};

// add the supplied rle section to the appropriate vector, which might
// need to be created. we are writing out [bes->wrote, curx) (i.e. curx
// ought *not* describe the |bes| element, and ought equal |dimx| when
// finalizing the band). caller must update bes->wrote afterwards!
static inline char*
sixelband_extend(char* vec, struct band_extender* bes, int dimx, int curx){
  assert(dimx >= bes->rle);
  assert(0 <= bes->rle);
  assert(0 <= bes->rep);
  assert(64 > bes->rep);
  if(vec == NULL){
    // FIXME for now we make it as big as it could possibly need to be. ps,
    // don't try to just base it off how far in we are; wipe/restore could
    // change that!
    if((vec = malloc(dimx + 1)) == NULL){
      return NULL;
    }
  }
  // rle will equal 0 if this is our first non-zero rep, at a non-zero x;
  // in that case, rep is guaranteed to be 0; catch it at the bottom.
  write_rle(vec, &bes->length, bes->rle, bes->rep + 63);
  int clearlen = curx - (bes->rle + bes->wrote);
  write_rle(vec, &bes->length, clearlen, '?');
  return vec;
}

// write to this cell's auxvec, backing up the pixels cleared by a wipe. wipes
// are requested at cell granularity, broken down into sixelbands, broken down
// by color, and then finally effected at the sixel RLE level. we're thus in
// any given call handling a horizontal contiguous range of sixels for a single
// color. the x range is wholly within the cell to be wiped, but the y range
// might not be, since cells and bands don't necessarily line up. |y| ought be
// the row of the first pixel of the *band*.
//
// we thus need:
//  - the starting and ending true x positions for the *portion of this sixel
//     contained within the wiped cell*.
//  - the true y position at which the sixel starts.
//  - the previous sixel rep and the masked sixel rep--the difference between
//     the two tells us which rows (offset from y) need be written. they ought
//     be the binary forms, not the presentation forms (i.e. [0..63]).
//  - the cell-pixel geometry, necessary for computing offset into the auxvec.
//  - the color.
//
// precondition: mask is a bitwise proper subset of rep
//
// we find which [1..6] of six rows are affected by examining the difference
// between |rep| and |masked|, the sixel's row within the cell by taking |y|
// modulo |cellpxy|, and the position within the auxvec by multiplying that
// result by |cellpxx| and adding |x| modulo |cellpxx|. we set |len| pixels.
static inline void
write_auxvec(uint8_t* auxvec, uint16_t color, int endy, int y, int x, int len,
             char rep, char masked, int cellpxy, int cellpxx){
  rep -= 63;
  masked -= 63;
  const char diff = rep ^ masked;
//fprintf(stderr, "AUXVEC WRITE[%hu] ey: %d y/x: %d/%d:%d r: 0x%x m: 0x%x d: 0x%x total %d\n", color, endy, y, x, len, rep, masked, diff, cellpxy * cellpxx);
  const int xoff = x % cellpxx;
  const int yoff = y % cellpxy;
  int dy = 0;
  for(char bitselector = 1 ; bitselector < 0x40 ; bitselector <<= 1u, ++dy){
    if((diff & bitselector) == 0){
//if(diff == 0x20)fprintf(stderr, "diff: 0x%x bs: %d\n", diff, bitselector);
      continue;
    }
    if(yoff + dy == endy){ // reached the next cell below
//if(diff == 0x20)fprintf(stderr, "BOUNCING! 0x%x bs: %d %d > %d\n", diff, bitselector, yoff + dy, cellpxy);
      break;
    }
//fprintf(stderr, " writing to auxrow %d (%d)\n", yoff + dy, bitselector);
    const int idx = (((yoff + dy) % cellpxy) * cellpxx + xoff) * AUXVECELEMSIZE;
//fprintf(stderr, " xoff: %d yoff: %d dy: %d ydy: %d idx: %d\n", xoff, yoff, dy, yoff + dy, idx);
    for(int i = 0 ; i < len ; ++i){
      memcpy(&auxvec[idx + i * AUXVECELEMSIZE], &color, AUXVECELEMSIZE);
    }
  }
}

// wipe the color within this band from startx to endx - 1, from starty to
// endy - 1 (0-offset in the band, a cell-sized region), writing out the
// auxvec. mask is the allowable sixel, y-wise. returns a positive number if
// pixels were wiped.
static inline int
wipe_color(sixelband* b, int color, int y, int endy, int startx, int endx,
           char mask, int dimx, uint8_t* auxvec,
           int cellpxy, int cellpxx){
  const char* vec = b->vecs[color];
  if(vec == NULL){
    return 0; // no work to be done here
  }
  int wiped = 0;
  char* newvec = malloc(dimx + 1);
  if(newvec == NULL){
    return -1;
  }
//fprintf(stderr, "color: %d Y: %d-%d X: %d-%d\n", color, starty, endy, startx, endx);
//fprintf(stderr, "s/e: %d/%d mask: %02x WIPE: [%s]\n", starty, endy, mask, vec);
  // we decode the color within the sixelband, and rebuild it without the
  // wiped pixels.
  int rle = 0; // the repetition number for this element
  // the x coordinate through which we've checked this band. if x + rle is
  // less than startx, this element cannot be affected by the wipe.
  // otherwise, starting at startx, it can be affected. once x >= endx, we
  // are done, and can copy any remaining elements blindly.
  int x = 0;
  int voff = 0;
  while(*vec){
    if(isdigit(*vec)){
      rle *= 10;
      rle += (*vec - '0');
    }else if(*vec == '!'){
      rle = 0;
    }else{
      if(rle == 0){
        rle = 1;
      }
      char rep = *vec;
      char masked = ((rep - 63) & mask) + 63;
//fprintf(stderr, "X/RLE/ENDX: %d %d %d\n", x, rle, endx);
      if(x + rle <= startx){ // not wiped material; reproduce as-is
        write_rle(newvec, &voff, rle, rep);
        x += rle;
      }else if(masked == rep){ // not changed by wipe; reproduce as-is
        write_rle(newvec, &voff, rle, rep);
        x += rle;
      }else{ // changed by wipe; might have to break it up
        wiped = 1;
        if(x < startx){
          write_rle(newvec, &voff, startx - x, rep);
          rle -= startx - x;
          x = startx;
        }
        if(x + rle >= endx){
          // FIXME this might equal the prev/next rep, and we ought combine
//fprintf(stderr, "************************* %d %d %d\n", endx - x, x, rle);
          write_rle(newvec, &voff, endx - x, masked);
          write_auxvec(auxvec, color, endy, y, x, endx - x, rep, masked, cellpxy, cellpxx);
          rle -= endx - x;
          x = endx;
        }else{
          write_rle(newvec, &voff, rle, masked);
          write_auxvec(auxvec, color, endy, y, x, rle, rep, masked, cellpxy, cellpxx);
          x += rle;
          rle = 0;
        }
        if(rle){
          write_rle(newvec, &voff, rle, rep);
          x += rle;
        }
      }
      rle = 0;
    }
    ++vec;
    if(x >= endx){
      strcpy(newvec + voff, vec); // there is always room
      break;
    }
  }
//if(strcmp(newvec, b->vecs[color])) fprintf(stderr, "WIPED %d y [%d..%d) x [%d..%d) mask: %d [%s]\n", color, starty, endy, startx, endx, mask, newvec);
  free(b->vecs[color]);
  if(voff == 0){
    // FIXME check for other null vectors; free such, and assign NULL
    free(newvec);
    newvec = NULL;
  }
  b->vecs[color] = newvec;
  return wiped;
}

// wipe the band from startx to endx - 1, from starty to endy - 1. returns the
// number of pixels actually wiped.
static inline int
wipe_band(sixelmap* smap, int band, int startx, int endx,
          int starty, int endy, int dimx, int cellpxy, int cellpxx,
          uint8_t* auxvec){
  int wiped = 0;
  // get 0-offset start and end row bounds for our band.
  const int sy = band * 6 < starty ? starty - band * 6 : 0;
  const int ey = (band + 1) * 6 > endy ? 6 - ((band + 1) * 6 - endy) : 6;
  // we've got a mask that we'll AND with the decoded sixels; set it to
  // 0 wherever we're wiping.
  unsigned char mask = 63;
  // knock out a bit for each row we're wiping within the band
  for(int i = 0 ; i < 6 ; ++i){
    if(i >= sy && i < ey){
      mask &= ~(1u << i);
    }
  }
//fprintf(stderr, "******************** BAND %d MASK 0x%x ********************8\n", band, mask);
  sixelband* b = &smap->bands[band];
  // offset into map->data where our color starts
  for(int i = 0 ; i < b->size ; ++i){
    wiped += wipe_color(b, i, band * 6, endy, startx, endx, mask,
                        dimx, auxvec, cellpxy, cellpxx);
  }
  return wiped;
}

// we return -1 because we're not doing a proper wipe -- that's not possible
// using sixel. we just mark it as partially transparent, so that if it's
// redrawn, it's redrawn using P2=1.
int sixel_wipe(sprixel* s, int ycell, int xcell){
//fprintf(stderr, "WIPING %d/%d\n", ycell, xcell);
  uint8_t* auxvec = sixel_trans_auxvec(ncplane_pile(s->n));
  if(auxvec == NULL){
    return -1;
  }
  const int cellpxy = ncplane_pile(s->n)->cellpxy;
  const int cellpxx = ncplane_pile(s->n)->cellpxx;
  sixelmap* smap = s->smap;
  const int startx = xcell * cellpxx;
  const int starty = ycell * cellpxy;
  int endx = ((xcell + 1) * cellpxx);
  if(endx >= s->pixx){
    endx = s->pixx;
  }
  int endy = ((ycell + 1) * cellpxy);
  if(endy >= s->pixy){
    endy = s->pixy;
  }
  const int startband = starty / 6;
  const int endband = (endy - 1) / 6;
//fprintf(stderr, "y/x: %d/%d bands: %d-%d start: %d/%d end: %d/%d\n", ycell, xcell, startband, endband - 1, starty, startx, endy, endx);
  // walk through each color, and wipe the necessary sixels from each band
  int w = 0;
  for(int b = startband ; b <= endband ; ++b){
    w += wipe_band(smap, b, startx, endx, starty, endy, s->pixx,
                   cellpxy, cellpxx, auxvec);
  }
  if(w){
    s->wipes_outstanding = true;
  }
  change_p2(s->glyph.buf, SIXEL_P2_TRANS);
  assert(NULL == s->n->tam[s->dimx * ycell + xcell].auxvector);
  s->n->tam[s->dimx * ycell + xcell].auxvector = auxvec;
  // FIXME this invalidation ought not be necessary, since we're simply
  // wiping, and thus a glyph is going to be printed over whatever we've
  // just destroyed. in alacritty, however, this isn't sufficient to knock
  // out a graphic; we need repaint with the transparency.
  // see https://github.com/dankamongmen/notcurses/issues/2142
  int absx, absy;
  ncplane_abs_yx(s->n, &absy, &absx);
  sprixel_invalidate(s, absy, absx);
  return 0;
}

// rebuilds the auxiliary vectors, and scrubs the actual pixels, following
// extraction of the palette. doing so allows the new frame's pixels to
// contribute to the solved palette, even if they were wiped in the previous
// frame. pixels ought thus have been set up in sixel_blit(), despite TAM
// entries in the ANNIHILATED state.
static int
scrub_color_table(sprixel* s){
  if(s->n && s->n->tam){
    // we use the sprixel cell geometry rather than the plane's because this
    // is called during our initial blit, before we've resized the plane.
    for(unsigned y = 0 ; y < s->dimy ; ++y){
      for(unsigned x = 0 ; x < s->dimx ; ++x){
        unsigned txyidx = y * s->dimx + x;
        sprixcell_e state = s->n->tam[txyidx].state;
        if(state == SPRIXCELL_ANNIHILATED || state == SPRIXCELL_ANNIHILATED_TRANS){
//fprintf(stderr, "POSTEXTRACT WIPE %d/%d\n", y, x);
          sixel_wipe(s, y, x);
        }
      }
    }
  }
  return 0;
}

// goes through the needs_refresh matrix, and damages cells needing refreshing.
void sixel_refresh(const ncpile* p, sprixel* s){
  if(s->needs_refresh == NULL){
    return;
  }
  int absy, absx;
  ncplane_abs_yx(s->n, &absy, &absx);
  for(unsigned y = 0 ; y < s->dimy ; ++y){
    const unsigned yy = absy + y;
    for(unsigned x = 0 ; x < s->dimx ; ++x){
      unsigned idx = y * s->dimx + x;
      if(s->needs_refresh[idx]){
        const unsigned xx = absx + x;
        if(xx < p->dimx && yy < p->dimy){
          unsigned ridx = yy * p->dimx + xx;
          struct crender *r = &p->crender[ridx];
          r->s.damaged = 1;
        }
      }
    }
  }
  free(s->needs_refresh);
  s->needs_refresh = NULL;
}

// when we first cross into a new cell, we check its old state, and if it
// was transparent, set the rmatrix low. otherwise, set it high. this should
// only be called for the first pixel in each cell.
static inline void
update_rmatrix(unsigned char* rmatrix, int txyidx, const tament* tam){
  if(rmatrix == NULL){
    return;
  }
  sprixcell_e state = tam[txyidx].state;
  if(state == SPRIXCELL_TRANSPARENT || state > SPRIXCELL_ANNIHILATED){
    rmatrix[txyidx] = 0;
  }else{
    rmatrix[txyidx] = 1;
  }
}

static int
qnodecmp(const void* q0, const void* q1){
  const qnode* qa = q0;
  const qnode* qb = q1;
  return qa->q.pop < qb->q.pop ? -1 : qa->q.pop == qb->q.pop ? 0 : 1;
}

// from the initial set of QNODECOUNT qnodes, extract the number of active
// ones -- our initial (reduced) color count -- and sort. heap allocation.
// precondition: colors > 0
static qnode*
get_active_set(qstate* qs, uint32_t colors){
  qnode* act = malloc(sizeof(*act) * colors);
  unsigned targidx = 0;
  // filter the initial qnodes for pop != 0
  unsigned total = QNODECOUNT + (qs->dynnodes_total - qs->dynnodes_free);
//fprintf(stderr, "TOTAL IS %u WITH %u COLORS\n", total, colors);
  for(unsigned z = 0 ; z < total && targidx < colors ; ++z){
//fprintf(stderr, "EXTRACT? [%04u] pop %u\n", z, qs->qnodes[z].q.pop);
    if(qs->qnodes[z].q.pop){
      memcpy(&act[targidx], &qs->qnodes[z], sizeof(*act));
      // link it back to the original node's position in the octree
//fprintf(stderr, "LINKING %u to %u\n", targidx, z);
      act[targidx].qlink = z;
      ++targidx;
    }else if(qs->qnodes[z].qlink){
      const struct onode* o = &qs->onodes[qs->qnodes[z].qlink - 1];
      // FIXME i don't think we need the second conditional? in a perfect world?
      for(unsigned s = 0 ; s < 8 && targidx < colors ; ++s){
//fprintf(stderr, "o: %p qlink: %u\n", o, qs->qnodes[z].qlink - 1);
        if(o->q[s]){
          memcpy(&act[targidx], o->q[s], sizeof(*act));
//fprintf(stderr, "O-LINKING %u to %ld[%u]\n", targidx, o->q[s] - qs->qnodes, s);
          act[targidx].qlink = o->q[s] - qs->qnodes;
          ++targidx;
        }
      }
    }
  }
//fprintf(stderr, "targidx: %u colors: %u\n", targidx, colors);
  assert(targidx == colors);
  qsort(act, colors, sizeof(*act), qnodecmp);
  return act;
}

static inline int
find_next_lowest_chosen(const qstate* qs, int z, int i, const qnode** hq){
//fprintf(stderr, "FIRST CHOSEN: %u %d\n", z, i);
  do{
    const qnode* h = &qs->qnodes[z];
//fprintf(stderr, "LOOKING AT %u POP %u QLINK %u CIDX %u\n", z, h->q.pop, h->qlink, h->cidx);
    if(h->q.pop == 0 && h->qlink){
      const onode* o = &qs->onodes[h->qlink - 1];
      while(i >= 0){
        h = o->q[i];
        if(h && chosen_p(h)){
          *hq = h;
//fprintf(stderr, "NEW HQ: %p RET: %u\n", *hq, z * 8 + i);
          return z * 8 + i;
        }
        if(++i == 8){
          break;
        }
      }
    }else{
      if(chosen_p(h)){
        *hq = h;
//fprintf(stderr, "NEW HQ: %p RET: %u\n", *hq, z * 8);
        return z * 8;
      }
    }
    ++z;
    i = 0;
  }while(z < QNODECOUNT);
//fprintf(stderr, "RETURNING -1\n");
  return -1;
}

static inline void
choose(qstate* qs, qnode* q, int z, int i, int* hi, int* lo,
       const qnode** hq, const qnode** lq){
  if(!chosen_p(q)){
//fprintf(stderr, "NOT CHOSEN: %u %u %u %u\n", z, qs->qnodes[z].qlink, qs->qnodes[z].q.pop, qs->qnodes[z].cidx);
    if(z * 8 > *hi){
      *hi = find_next_lowest_chosen(qs, z, i, hq);
    }
    int cur = z * 8 + (i >= 0 ? i : 4);
    if(*lo == -1){
      q->cidx = qidx(*hq);
    }else if(*hi == -1 || cur - *lo < *hi - cur){
      q->cidx = qidx(*lq);
    }else{
      q->cidx = qidx(*hq);
    }
  }else{
    *lq = q;
    *lo = z * 8;
  }
}

// we must reduce the number of colors until we're using less than or equal
// to the number of color registers.
static inline int
merge_color_table(qstate* qs){
  if(qs->smap->colors == 0){
    return 0;
  }
  qnode* qactive = get_active_set(qs, qs->smap->colors);
  if(qactive == NULL){
    return -1;
  }
  // assign color table entries to the most popular colors. use the lowest
  // color table entries for the most popular ones, as they're the shortest
  // (this is not necessarily an optimizing huristic, but it'll do for now).
  int cidx = 0;
//fprintf(stderr, "colors: %u cregs: %u\n", qs->colors, colorregs);
  for(int z = qs->smap->colors - 1 ; z >= 0 ; --z){
    if(qs->smap->colors >= qs->bargs->u.pixel.colorregs){
      if(cidx == qs->bargs->u.pixel.colorregs){
        break; // we just ran out of color registers
      }
    }
    qs->qnodes[qactive[z].qlink].cidx = make_chosen(cidx);
    ++cidx;
  }
  free(qactive);
  if(qs->smap->colors > qs->bargs->u.pixel.colorregs){
    // tend to those which couldn't get a color table entry. we start with two
    // values, lo and hi, initialized to -1. we iterate over the *static* qnodes,
    // descending into onodes to check their qnodes. we thus iterate over all
    // used qnodes, in order (and also unused static qnodes). if the node is
    // empty, continue. if it is chosen, replace lo. otherwise, if hi is less
    // than z, we need find the next lowest chosen one. if there is no next
    // lowest, hi is reset to -1. otherwise, set hi. once we have the new hi > z,
    // determine which of hi and lo are closer to z, discounting -1 values, and
    // link te closer one to z. a toplevel node is worth 8 in terms of distance;
    // and lowlevel node is worth 1.
    int lo = -1;
    int hi = -1;
    const qnode* lq = NULL;
    const qnode* hq = NULL;
    for(int z = 0 ; z < QNODECOUNT ; ++z){
      if(qs->qnodes[z].q.pop == 0){
        if(qs->qnodes[z].qlink == 0){
          continue; // unused
        }
        // process the onode
        const onode* o = &qs->onodes[qs->qnodes[z].qlink - 1];
        for(int i = 0 ; i < 8 ; ++i){
          if(o->q[i]){
            choose(qs, o->q[i], z, i, &hi, &lo, &hq, &lq);
          }
        }
      }else{
        choose(qs, &qs->qnodes[z], z, -1, &hi, &lo, &hq, &lq);
      }
    }
    qs->smap->colors = qs->bargs->u.pixel.colorregs;
  }
  return 0;
}

static inline void
load_color_table(const qstate* qs){
  int loaded = 0;
  int total = QNODECOUNT + (qs->dynnodes_total - qs->dynnodes_free);
  for(int z = 0 ; z < total && loaded < qs->smap->colors ; ++z){
    const qnode* q = &qs->qnodes[z];
    if(chosen_p(q)){
      qs->table[RGBSIZE * qidx(q) + 0] = ss(q->q.comps[0]);
      qs->table[RGBSIZE * qidx(q) + 1] = ss(q->q.comps[1]);
      qs->table[RGBSIZE * qidx(q) + 2] = ss(q->q.comps[2]);
      ++loaded;
    }
  }
//fprintf(stderr, "loaded: %u colors: %u\n", loaded, qs->colors);
  assert(loaded == qs->smap->colors);
}

// build up a sixel band from (up to) 6 rows of the source RGBA.
static inline int
build_sixel_band(qstate* qs, int bnum){
//fprintf(stderr, "building band %d\n", bnum);
  sixelband* b = &qs->smap->bands[bnum];
  b->size = qs->smap->colors;
  size_t bsize = sizeof(*b->vecs) * b->size;
  size_t mlen = qs->smap->colors * sizeof(struct band_extender);
  struct band_extender* meta = malloc(mlen);
  if(meta == NULL){
    return -1;
  }
  b->vecs = malloc(bsize);
  if(b->vecs == NULL){
    free(meta);
    return -1;
  }
  memset(b->vecs, 0, bsize);
  memset(meta, 0, mlen);
  const int ystart = qs->bargs->begy + bnum * 6;
  const int endy = (bnum + 1 == qs->smap->sixelbands ?
                                 qs->leny - qs->bargs->begy : ystart + 6);
  struct {
    int color; // 0..colormax
    int rep;   // non-zero representation, 1..63
  } active[6];
  // we're going to advance horizontally through the sixelband
  int x;
  // FIXME we could greatly clean this up by tracking, for each color, the active
  // rep and the number of times we've seen it...but only write it out either (a)
  // when the rep changes (b) when we get the color again after a gap or (c) at the
  // end. that way we wouldn't need maintain these prevactive/active sets...
  for(x = qs->bargs->begx ; x < (qs->bargs->begx + qs->lenx) ; ++x){ // pixel column
    // there are at most 6 colors represented in any given sixel. at each
    // sixel, we need to *start tracking* new colors, and colors which changed
    // their representation. we also write out what we previously tracked for
    // this color: possibly a non-zero rep, possibly followed by a zero-rep (we
    // can have zero, either, or both).
    int activepos = 0; // number of active entries used
    for(int y = ystart ; y < endy ; ++y){
      const uint32_t* rgb = (qs->data + (qs->linesize / 4 * y) + x);
      if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
        continue;
      }
      int cidx = find_color(qs, *rgb);
      if(cidx < 0){
        // FIXME free?
        return -1;
      }
      int act;
      for(act = 0 ; act < activepos ; ++act){
        if(active[act].color == cidx){
          active[act].rep |= (1u << (y - ystart));
          break;
        }
      }
      if(act == activepos){ // didn't find it; create new entry
        active[activepos].color = cidx;
        active[activepos].rep = (1u << (y - ystart));
        ++activepos;
      }
    }
    // we now have the active set. check to see if they extend existing RLEs,
    // and if not, write out whatever came before us.
    for(int i = 0 ; i < activepos ; ++i){
      const int c = active[i].color;
      if(meta[c].rep == active[i].rep && meta[c].rle + meta[c].wrote == x){
        ++meta[c].rle;
      }else{
        b->vecs[c] = sixelband_extend(b->vecs[c], &meta[c], qs->lenx, x);
        if(b->vecs[c] == NULL){
          return -1;
        }
        meta[c].rle = 1;
        meta[c].wrote = x;
        meta[c].rep = active[i].rep;
      }
    }
  }
  for(int i = 0 ; i < qs->smap->colors ; ++i){
    if(meta[i].rle){ // color was wholly unused iff rle == 0 at end
      b->vecs[i] = sixelband_extend(b->vecs[i], &meta[i], qs->lenx, x);
      if(b->vecs[i] == NULL){
        return -1;
      }
    }else{
      b->vecs[i] = NULL;
    }
  }
  free(meta);
  return 0;
}

static int
bandworker(qstate* qs){
  int b;
  while((b = qs->bandbuilder++) < qs->smap->sixelbands){
    if(build_sixel_band(qs, b) < 0){
      return -1;
    }
  }
  return 0;
}

// we have converged upon some number of colors. we now run over the pixels
// once again, and get the actual (color-indexed) sixels.
static inline int
build_data_table(sixel_engine* sengine, qstate* qs){
  sixelmap* smap = qs->smap;
  if(smap->sixelbands == 0){
    logerror("no sixels");
    return -1;
  }
  qs->bandbuilder = 0;
  enqueue_to_workers(sengine, qs);
  size_t tsize = RGBSIZE * smap->colors;
  qs->table = malloc(tsize);
  if(qs->table == NULL){
    return -1;
  }
  load_color_table(qs);
  bandworker(qs);
  block_on_workers(sengine, qs);
  return 0;
}

static inline int
extract_cell_color_table(qstate* qs, long cellid){
  const int ccols = qs->bargs->u.pixel.spx->dimx;
  const long x = cellid % ccols;
  const long y = cellid / ccols;
  const int cdimy = qs->bargs->u.pixel.cellpxy;
  const int cdimx = qs->bargs->u.pixel.cellpxx;
  const int begy = qs->bargs->begy;
  const int begx = qs->bargs->begx;
  const int leny = qs->leny;
  const int lenx = qs->lenx;
  const int cstartx = begx + x * cdimx; // starting pixel col for cell
  const int cstarty = begy + y * cdimy; // starting pixel row for cell
  typeof(qs->bargs->u.pixel.spx->needs_refresh) rmatrix = qs->bargs->u.pixel.spx->needs_refresh;
  tament* tam = qs->bargs->u.pixel.spx->n->tam;
  int cendy = cstarty + cdimy;    // one past last pixel row for cell
  if(cendy > begy + leny){
    cendy = begy + leny;
  }
  int cendx = cstartx + cdimx;      // one past last pixel col for cell
  if(cendx > begx + lenx){
    cendx = begx + lenx;
  }
  // we initialize the TAM entry based on the first pixel. if it's transparent,
  // initialize as transparent, and otherwise as opaque. following that, any
  // transparent pixel takes opaque to mixed, and any filled pixel takes
  // transparent to mixed.
  if(cstarty >= cendy){ // we're entirely transparent sixel overhead
    tam[cellid].state = SPRIXCELL_TRANSPARENT;
    qs->smap->p2 = SIXEL_P2_TRANS; // even one forces P2=1
    // FIXME need we set rmatrix?
    return 0;
  }
  const uint32_t* rgb = (qs->data + (qs->linesize / 4 * cstarty) + cstartx);
  if(tam[cellid].state == SPRIXCELL_ANNIHILATED || tam[cellid].state == SPRIXCELL_ANNIHILATED_TRANS){
    if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
      update_rmatrix(rmatrix, cellid, tam);
      tam[cellid].state = SPRIXCELL_ANNIHILATED_TRANS;
      free(tam[cellid].auxvector);
      tam[cellid].auxvector = NULL;
    }else{
      update_rmatrix(rmatrix, cellid, tam);
      free(tam[cellid].auxvector);
      tam[cellid].auxvector = NULL;
    }
  }else{
    if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
      update_rmatrix(rmatrix, cellid, tam);
      tam[cellid].state = SPRIXCELL_TRANSPARENT;
    }else{
      update_rmatrix(rmatrix, cellid, tam);
      tam[cellid].state = SPRIXCELL_OPAQUE_SIXEL;
    }
  }
  for(int visy = cstarty ; visy < cendy ; ++visy){   // current abs pixel row
    for(int visx = cstartx ; visx < cendx ; ++visx){ // current abs pixel col
      rgb = (qs->data + (qs->linesize / 4 * visy) + visx);
      // we do *not* exempt already-wiped pixels from palette creation. once
      // we're done, we'll call sixel_wipe() on these cells. so they remain
      // one of SPRIXCELL_ANNIHILATED or SPRIXCELL_ANNIHILATED_TRANS.
      // intentional bitwise or, to avoid dependency
      if(tam[cellid].state != SPRIXCELL_ANNIHILATED){
        if(tam[cellid].state == SPRIXCELL_ANNIHILATED_TRANS){
          if(!rgba_trans_p(*rgb, qs->bargs->transcolor)){
            tam[cellid].state = SPRIXCELL_ANNIHILATED;
          }
        }else{
          if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
            if(tam[cellid].state == SPRIXCELL_OPAQUE_SIXEL){
              tam[cellid].state = SPRIXCELL_MIXED_SIXEL;
            }
          }else{
            if(tam[cellid].state == SPRIXCELL_TRANSPARENT){
              tam[cellid].state = SPRIXCELL_MIXED_SIXEL;
            }
          }
        }
      }
//fprintf(stderr, "vis: %d/%d\n", visy, visx);
      if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
        continue;
      }
      if(insert_color(qs, *rgb)){
        return -1;
      }
    }
  }
  // if we're opaque, we needn't clear the old cell with a glyph
  if(tam[cellid].state == SPRIXCELL_OPAQUE_SIXEL){
    rmatrix[cellid] = 0;
  }else{
    qs->smap->p2 = SIXEL_P2_TRANS; // even one forces P2=1
  }
  return 0;
}

// we have a 4096-element array that takes the 4-5-3 MSBs from the RGB
// components. once it's complete, we might need to either merge some
// chunks, or expand them, converging towards the available number of
// color registers. |ccols| is cell geometry; |leny| and |lenx| are pixel
// geometry, and *do not* include sixel padding.
static int
extract_color_table(sixel_engine* sengine, qstate* qs){
  const blitterargs* bargs = qs->bargs;
  // use the cell geometry as computed by the visual layer; leny doesn't
  // include any mandatory sixel padding.
  const int crows = bargs->u.pixel.spx->dimy;
  const int ccols = bargs->u.pixel.spx->dimx;
  typeof(bargs->u.pixel.spx->needs_refresh) rmatrix;
  rmatrix = malloc(sizeof(*rmatrix) * crows * ccols);
  if(rmatrix == NULL){
    return -1;
  }
  bargs->u.pixel.spx->needs_refresh = rmatrix;
  long cellid = 0;
  for(int y = 0 ; y < crows ; ++y){ // cell row
    for(int x = 0 ; x < ccols ; ++x){ // cell column
      if(extract_cell_color_table(qs, cellid)){
        return -1;
      }
      ++cellid;
    }
  }
  loginfo("octree got %"PRIu32" entries", qs->smap->colors);
  if(merge_color_table(qs)){
    return -1;
  }
  if(build_data_table(sengine, qs)){
    return -1;
  }
  loginfo("final palette: %u/%u colors", qs->smap->colors, qs->bargs->u.pixel.colorregs);
  return 0;
}

static inline int
write_sixel_intro(fbuf* f, sixel_p2_e p2, int leny, int lenx){
  int rr, r = fbuf_puts(f, "\x1bP0;");
  if(r < 0){
    return -1;
  }
  if((rr = fbuf_putint(f, p2)) < 0){
    return -1;
  }
  r += rr;
  if((rr = fbuf_puts(f, ";0q\"1;1;")) < 0){
    return -1;
  }
  r += rr;
  if((rr = fbuf_putint(f, lenx)) < 0){
    return -1;
  }
  r += rr;
  if(fbuf_putc(f, ';') != 1){
    return -1;
  }
  ++r;
  if((rr = fbuf_putint(f, leny)) < 0){
    return -1;
  }
  r += rr;
  return r;
}

// write a single color register. rc/gc/bc are on [0..100].
static inline int
write_sixel_creg(fbuf* f, int idx, int rc, int gc, int bc){
  int rr, r = 0;
  if(fbuf_putc(f, '#') != 1){
    return -1;
  }
  ++r;
  if((rr = fbuf_putint(f, idx)) < 0){
    return -1;
  }
  r += rr;
  if((rr = fbuf_puts(f, ";2;")) < 0){
    return -1;
  }
  r += rr;
  if((rr = fbuf_putint(f, rc)) < 0){
    return -1;
  }
  r += rr;
  if(fbuf_putc(f, ';') != 1){
    return -1;
  }
  ++r;
  if((rr = fbuf_putint(f, gc)) < 0){
    return -1;
  }
  r += rr;
  if(fbuf_putc(f, ';') != 1){
    return -1;
  }
  ++r;
  if((rr = fbuf_putint(f, bc)) < 0){
    return -1;
  }
  r += rr;
  return r;
}

// write the escape which opens a Sixel, plus the palette table. returns the
// number of bytes written, so that this header can be directly copied in
// future reencodings. |leny| and |lenx| are output pixel geometry.
// returns the number of bytes written, so it can be stored at *parse_start.
static int
write_sixel_header(qstate* qs, fbuf* f, int leny){
  if(leny % 6){
    return -1;
  }
  // Set Raster Attributes - pan/pad=1 (pixel aspect ratio), Ph=qs->lenx, Pv=leny
  int r = write_sixel_intro(f, qs->smap->p2, leny, qs->lenx);
  if(r < 0){
    return -1;
  }
  for(int i = 0 ; i < qs->smap->colors ; ++i){
    const unsigned char* rgb = qs->table + i * RGBSIZE;
    //fprintf(fp, "#%d;2;%u;%u;%u", i, rgb[0], rgb[1], rgb[2]);
    int rr = write_sixel_creg(f, i, rgb[0], rgb[1], rgb[2]);
    if(rr < 0){
      return -1;
    }
    r += rr;
  }
  return r;
}

static int
write_sixel_payload(fbuf* f, const sixelmap* map){
  for(int j = 0 ; j < map->sixelbands ; ++j){
    int needclosure = 0;
    const sixelband* band = &map->bands[j];
    for(int i = 0 ; i < band->size ; ++i){
      if(band->vecs[i]){
        if(needclosure){
          if(fbuf_putc(f, '$') != 1){ // end previous one
            return -1;
          }
        }else{
          needclosure = 1;
        }
        if(fbuf_putc(f, '#') != 1){
          return -1;
        }
        if(fbuf_putint(f, i) < 0){
          return -1;
        }
        if(fbuf_puts(f, band->vecs[i]) < 0){
          return -1;
        }
      }
    }
    if(fbuf_putc(f, '-') != 1){
      return -1;
    }
  }
  if(fbuf_puts(f, "\e\\") < 0){
    return -1;
  }
  return 0;
}

// once per render cycle (if needed), make the actual payload match the TAM. we
// don't do these one at a time due to the complex (expensive) process involved
// in regenerating a sixel (we can't easily do it in-place). anything newly
// ANNIHILATED (state is ANNIHILATED, but no auxvec present) is dropped from
// the payload, and an auxvec is generated. anything newly restored (state is
// OPAQUE_SIXEL or MIXED_SIXEL, but an auxvec is present) is restored to the
// payload, and the auxvec is freed. none of this takes effect until the sixel
// is redrawn, and annihilated sprixcells still require a glyph to be emitted.
static inline int
sixel_reblit(sprixel* s){
  fbuf_chop(&s->glyph, s->parse_start);
  if(write_sixel_payload(&s->glyph, s->smap) < 0){
    return -1;
  }
  change_p2(s->glyph.buf, s->smap->p2);
  return 0;
}

// write out the sixel header after having quantized the palette.
static inline int
sixel_blit_inner(qstate* qs, sixelmap* smap, const blitterargs* bargs, tament* tam){
  fbuf f;
  if(fbuf_init(&f)){
    return -1;
  }
  sprixel* s = bargs->u.pixel.spx;
  const int cellpxy = bargs->u.pixel.cellpxy;
  const int cellpxx = bargs->u.pixel.cellpxx;
  int outy = qs->leny;
  if(outy % 6){
    outy += 6 - (qs->leny % 6);
    smap->p2 = SIXEL_P2_TRANS;
  }
  int parse_start = write_sixel_header(qs, &f, outy);
  if(parse_start < 0){
    fbuf_free(&f);
    return -1;
  }
  // we don't write out the payload yet -- set wipes_outstanding high, and
  // it'll be emitted via sixel_reblit(), taking into account any wipes that
  // occurred before it was displayed. otherwise, such a wipe would require
  // two emissions, one of which would be thrown away.
  scrub_tam_boundaries(tam, outy, qs->lenx, cellpxy, cellpxx);
  // take ownership of buf on success
  if(plane_blit_sixel(s, &f, outy, qs->lenx, parse_start, tam, SPRIXEL_INVALIDATED) < 0){
    fbuf_free(&f);
    return -1;
  }
  s->smap = smap;
  return 1;
}

// |leny| and |lenx| are the scaled output geometry. we take |leny| up to the
// nearest multiple of six greater than or equal to |leny|.
int sixel_blit(ncplane* n, int linesize, const void* data, int leny, int lenx,
               const blitterargs* bargs){
  if(bargs->u.pixel.colorregs >= TRANS_PALETTE_ENTRY){
    logerror("palette too large %d", bargs->u.pixel.colorregs);
    return -1;
  }
  sixelmap* smap = sixelmap_create(leny - bargs->begy);
  if(smap == NULL){
    return -1;
  }
  assert(n->tam);
  qstate* qs;
  if((qs = alloc_qstate(bargs->u.pixel.colorregs)) == NULL){
    logerror("couldn't allocate qstate");
    sixelmap_free(smap);
    return -1;
  }
  qs->bargs = bargs;
  qs->data = data;
  qs->linesize = linesize;
  qs->smap = smap;
  qs->leny = leny;
  qs->lenx = lenx;
  sixel_engine* sengine = ncplane_pile(n) ? ncplane_notcurses(n)->tcache.sixelengine : NULL;
  if(extract_color_table(sengine, qs)){
    free(bargs->u.pixel.spx->needs_refresh);
    bargs->u.pixel.spx->needs_refresh = NULL;
    sixelmap_free(smap);
    free_qstate(qs);
    return -1;
  }
  // takes ownership of sixelmap on success
  int r = sixel_blit_inner(qs, smap, bargs, n->tam);
  free_qstate(qs);
  if(r < 0){
    sixelmap_free(smap);
    // FIXME free refresh table?
  }
  scrub_color_table(bargs->u.pixel.spx);
  // we haven't actually emitted the body of the sixel yet. instead, we'll emit
  // it at sixel_redraw(), thus avoiding a double emission in the case of wipes
  // taking place before it's visible.
  bargs->u.pixel.spx->wipes_outstanding = 1;
  return r;
}

// to destroy a sixel, we damage all cells underneath it. we might not have
// to, though, if we've got a new sixel ready to go where the old sixel was
// (though we'll still need to if the new sprixcell not opaque, and the
// old and new sprixcell are different in any transparent pixel).
int sixel_scrub(const ncpile* p, sprixel* s){
  loginfo("%d state %d at %d/%d (%d/%d)", s->id, s->invalidated, s->movedfromy, s->movedfromx, s->dimy, s->dimx);
  int starty = s->movedfromy;
  int startx = s->movedfromx;
  for(int yy = starty ; yy < starty + (int)s->dimy && yy < (int)p->dimy ; ++yy){
    for(int xx = startx ; xx < startx + (int)s->dimx && xx < (int)p->dimx ; ++xx){
      int ridx = yy * p->dimx + xx;
      struct crender *r = &p->crender[ridx];
      if(!s->n){
        // need this to damage cells underneath a sprixel we're removing
        r->s.damaged = 1;
        continue;
      }
      sprixel* trues = r->sprixel ? r->sprixel : s;
      if(yy >= (int)trues->n->leny || yy - trues->n->absy < 0){
        r->s.damaged = 1;
        continue;
      }
      if(xx >= (int)trues->n->lenx || xx - trues->n->absx < 0){
        r->s.damaged = 1;
        continue;
      }
      sprixcell_e state = sprixel_state(trues, yy, xx);
//fprintf(stderr, "CHECKING %d/%d state: %d %d/%d\n", yy - s->movedfromy - s->n->absy, xx - s->movedfromx - s->n->absx, state, yy, xx);
      if(state == SPRIXCELL_TRANSPARENT || state == SPRIXCELL_MIXED_SIXEL){
        r->s.damaged = 1;
      }else if(s->invalidated == SPRIXEL_MOVED){
        // ideally, we wouldn't damage our annihilated sprixcells, but if
        // we're being annihilated only during this cycle, we need to go
        // ahead and damage it.
        r->s.damaged = 1;
      }
    }
  }
  return 1;
}

// returns the number of bytes written
int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
               int yoff, int xoff){
  (void)ti;
  // if we've wiped or rebuilt any cells, effect those changes now, or else
  // we'll get flicker when we move to the new location.
  if(s->wipes_outstanding){
    if(sixel_reblit(s)){
      return -1;
    }
    s->wipes_outstanding = false;
  }
  if(p){
    const int targy = s->n->absy + yoff;
    const int targx = s->n->absx + xoff;
    if(goto_location(p->nc, f, targy, targx, NULL)){
      return -1;
    }
    if(s->invalidated == SPRIXEL_MOVED){
      for(int yy = s->movedfromy ; yy < s->movedfromy + (int)s->dimy && yy < (int)p->dimy ; ++yy){
        if(yy < 0){
          continue;
        }
        for(int xx = s->movedfromx ; xx < s->movedfromx + (int)s->dimx && xx < (int)p->dimx ; ++xx){
          if(xx < 0){
            continue;
          }
          struct crender *r = &p->crender[yy * p->dimx + xx];
          if(!r->sprixel || sprixel_state(r->sprixel, yy, xx) != SPRIXCELL_OPAQUE_SIXEL){
            r->s.damaged = 1;
          }
        }
      }
    }
  }
  if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){
    return -1;
  }
  s->invalidated = SPRIXEL_QUIESCENT;
  return s->glyph.used;
}

// a quantization worker.
static void *
sixel_worker(void* v){
  work_queue* wq = v;
  sixel_engine *sengine = wq->sengine;

  qstate* qs = NULL;
  unsigned bufpos = 0; // index into worker queue
  do{
    pthread_mutex_lock(&sengine->lock);
    while(wq->used == 0 && !sengine->done){
      pthread_cond_wait(&sengine->cond, &sengine->lock);
    }
    if(!sengine->done){
      qs = wq->qstates[bufpos];
    }else{
      qs = NULL;
    }
    pthread_mutex_unlock(&sengine->lock);
    if(qs == NULL){
      break;
    }
    bandworker(qs);
    bool sendsignal = false;
    pthread_mutex_lock(&sengine->lock);
    --wq->used;
    if(--qs->refcount == 0){
      sendsignal = true;
    }
    pthread_mutex_unlock(&sengine->lock);
    if(sendsignal){
      pthread_cond_broadcast(&sengine->cond);
    }
    if(++bufpos == WORKERDEPTH){
      bufpos = 0;
    }
  }while(1);
  return NULL;
}

static int
sixel_init_core(tinfo* ti, const char* initstr, int fd){
  if((ti->sixelengine = malloc(sizeof(sixel_engine))) == NULL){
    return -1;
  }
  sixel_engine* sengine = ti->sixelengine;
  pthread_mutex_init(&sengine->lock, NULL);
  pthread_cond_init(&sengine->cond, NULL);
  sengine->done = false;
  const int workers_wanted = sizeof(sengine->tids) / sizeof(*sengine->tids);
  for(int w = 0 ; w < workers_wanted ; ++w){
    sengine->queues[w].sengine = sengine;
    sengine->queues[w].writeto = 0;
    sengine->queues[w].used = 0;
    if(pthread_create(&sengine->tids[w], NULL, sixel_worker, &sengine->queues[w])){
      logerror("couldn't spin up sixel worker %d/%d", w, workers_wanted);
      // FIXME kill any created workers
      return -1;
    }
  }
  return tty_emit(initstr, fd);
}

// private mode 80 (DECSDM) manages "Sixel Scrolling Mode" vs "Sixel Display
// Mode". when 80 is enabled (i.e. DECSDM mode), images are displayed at the
// upper left, and clipped to the window. we don't want either of those things
// to happen, so we explicitly disable DECSDM.
// private mode 8452 places the cursor at the end of a sixel when it's
//  emitted. we don't need this for rendered mode, but we do want it for
//  direct mode. it causes us no problems, so always set it.
int sixel_init_forcesdm(tinfo* ti, int fd){
  return sixel_init_core(ti, "\e[?80l\e[?8452h", fd);
}

int sixel_init_inverted(tinfo* ti, int fd){
  // some terminals, at some versions, invert the sense of DECSDM. for those,
  // we must use 80h rather than the correct 80l. this grows out of a
  // misunderstanding in XTerm through patchlevel 368, which was widely
  // copied into other terminals.
  return sixel_init_core(ti, "\e[?80h\e[?8452h", fd);
}

// if we aren't sure of the semantics of the terminal we're speaking with,
// don't touch DECSDM at all. it's almost certainly set up the way we want.
int sixel_init(tinfo* ti, int fd){
  return sixel_init_core(ti, "\e[?8452h", fd);
}

// restore the |yoff|th bit of the sixel at |xoff| for the specified vec
// FIXME this is a very dopey implementation yuck, use RLE at least
static int
restore_vec(sixelband* b, int color, int bit, int xoff, int dimx){
  if(color >= b->size){
    logpanic("illegal color %d >= %d", color, b->size);
    return -1;
  }
  char* v = NULL;
  const char* vec = b->vecs[color]; // might be NULL
  if(vec == NULL){ // write this sixel, and we're done
    struct band_extender bes = {
      .rle = 1,
      .rep = bit,
    };
    if((v = sixelband_extend(v, &bes, dimx, xoff)) == NULL){
      return -1;
    }
  }else{
    int rle = 0; // the repetition number for this element
    int x = 0;
    int voff = 0;
    if((v = malloc(dimx + 1)) == NULL){
      return -1;
    }
    while(*vec){
      if(isdigit(*vec)){
        rle *= 10;
        rle += (*vec - '0');
      }else if(*vec == '!'){
        rle = 0;
      }else{
        if(rle == 0){
          rle = 1;
        }
        char rep = *vec;
//fprintf(stderr, "X/RLE/ENDX: %d %d %d\n", x, rle, endx);
        if(x + rle <= xoff){ // not wiped material; reproduce as-is
          write_rle(v, &voff, rle, rep);
          x += rle;
        }else if(x > xoff){
          write_rle(v, &voff, rle, rep);
          x += rle;
        }else{
          if(x < xoff){
            write_rle(v, &voff, xoff - x, rep);
            rle -= xoff - x;
            x = xoff;
          }
          write_rle(v, &voff, 1, ((rep - 63) | bit) + 63);
          --rle;
          ++x;
          if(rle){
            write_rle(v, &voff, rle, rep);
            x += rle;
          }
        }
        rle = 0;
      }
      ++vec;
      if(x > xoff){
        strcpy(v + voff, vec); // there is always room
        break;
      }
    }
  }
  free(b->vecs[color]);
  b->vecs[color] = v;
//fprintf(stderr, "SET NEW VEC (%zu) [%s]\n", strlen(v), v);
  return 0;
}

// rebuild the portion of some cell which is within this band, having stored
// the pixels into the auxvec when the cell was wiped (and updated them if we
// loaded another frame). we go through the auxvec to the right and down,
// within the area covered by our band. if the entry is transparent, do
// nothing. otherwise, it is some color; collect other instances of the color,
// marking them transparent as we do so, and update that color's band. in
// the worst case (all pixels different colors), this will be p^2 =\ FIXME.
//
// returns the number of source-transparent pixels (i.e. pixels which weren't
// restored), which will be used to update the TAM state.
static inline int
restore_band(sixelmap* smap, int band, int startx, int endx,
             int starty, int endy, int dimx, int cellpxy, int cellpxx,
             uint8_t* auxvec){
  int restored = 0;
  const int sy = band * 6 < starty ? starty - band * 6 : 0;
  const int ey = (band + 1) * 6 > endy ? 6 - ((band + 1) * 6 - endy) : 6;
  const int width = endx - startx;
  const int height = ey - sy;
  const int totalpixels = width * height;
  sixelband* b = &smap->bands[band];
//fprintf(stderr, "RESTORING band %d (%d->%d (%d->%d), %d->%d) %d pixels\n", band, sy, ey, starty, endy, startx, endx, totalpixels);
  int yoff = ((band * 6) + sy - starty) % cellpxy; // we start off on this row of the auxvec
  int xoff = startx % cellpxx;
  for(int dy = sy ; dy < ey ; ++dy, ++yoff){
    const int idx = (yoff * cellpxx + xoff) * AUXVECELEMSIZE;
    const int bit = 1 << dy;
//fprintf(stderr, " looking at bandline %d (auxvec row %d idx %d, dy %d)\n", dy, yoff, idx, dy);
    for(int dx = 0 ; startx + dx < endx ; ++dx){
      uint16_t color;
      memcpy(&color, &auxvec[idx + dx * AUXVECELEMSIZE], AUXVECELEMSIZE);
//fprintf(stderr, "  idx %d (dx %d x %d): %hu\n", idx, dx, dx + startx, color);
      if(color != TRANS_PALETTE_ENTRY){
        restore_vec(b, color, bit, startx + dx, dimx);
        ++restored;
      }
    }
  }
  (void)smap;
  return totalpixels - restored;
}

// only called for cells in SPRIXCELL_ANNIHILATED[_TRANS]. just post to
// wipes_outstanding, so the Sixel gets regenerated the next render cycle,
// just like wiping. this is necessary due to the complex nature of
// modifying a Sixel -- we want to do them all in one batch.
int sixel_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
//fprintf(stderr, "REBUILDING %d/%d\n", ycell, xcell);
  if(auxvec == NULL){
    return -1;
  }
  const int cellpxy = ncplane_pile(s->n)->cellpxy;
  const int cellpxx = ncplane_pile(s->n)->cellpxx;
  sixelmap* smap = s->smap;
  const int startx = xcell * cellpxx;
  const int starty = ycell * cellpxy;
  int endx = ((xcell + 1) * cellpxx);
  if(endx >= s->pixx){
    endx = s->pixx;
  }
  int endy = ((ycell + 1) * cellpxy);
  if(endy >= s->pixy){
    endy = s->pixy;
  }
  const int startband = starty / 6;
  const int endband = (endy - 1) / 6;
//fprintf(stderr, "%d/%d start: %d/%d end: %d/%d bands: %d-%d\n", ycell, xcell, starty, startx, endy, endx, starty / 6, endy / 6);
  // walk through each color, and wipe the necessary sixels from each band
  int w = 0;
  for(int b = startband ; b <= endband ; ++b){
    w += restore_band(smap, b, startx, endx, starty, endy, s->pixx,
                      cellpxy, cellpxx, auxvec);
  }
  s->wipes_outstanding = true;
  sprixcell_e newstate;
  if(w == cellpxx * cellpxy){
    newstate = SPRIXCELL_TRANSPARENT;
  }else if(w){
    newstate = SPRIXCELL_MIXED_SIXEL;
  }else{
    newstate = SPRIXCELL_OPAQUE_SIXEL;
  }
  s->n->tam[s->dimx * ycell + xcell].state = newstate;
  return 1;
}

void sixel_cleanup(tinfo* ti){
  sixel_engine* sengine = ti->sixelengine;
  const unsigned tids = POPULATION;
  pthread_mutex_lock(&sengine->lock);
  sengine->done = 1;
  pthread_mutex_unlock(&sengine->lock);
  pthread_cond_broadcast(&sengine->cond);
  loginfo("joining %u sixel thread%s", tids, tids == 1 ? "" : "s");
  for(unsigned t = 0 ; t < tids ; ++t){
    pthread_join(sengine->tids[t], NULL);
  }
  pthread_mutex_destroy(&sengine->lock);
  pthread_cond_destroy(&sengine->cond);
  free(sengine);
  loginfo("reaped sixel engine");
  ti->sixelengine = NULL;
  // no way to know what the state was before; we ought use XTSAVE/XTRESTORE
}

// create an auxiliary vector suitable for a Sixel sprixcell, and zero it out.
// there are two bytes per pixel in the cell: a palette index of up to 65534,
// or 65535 to indicate transparency.
uint8_t* sixel_trans_auxvec(const ncpile* p){
  const size_t slen = AUXVECELEMSIZE * p->cellpxy * p->cellpxx;
  uint8_t* a = malloc(slen);
  if(a){
    memset(a, 0xff, slen);
  }
  return a;
}
