/*
    bomb - automatic interactive visual stimulation
    Copyright (C) 1994  Scott Draves <spot@cs.cmu.edu>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "defs.h"
#include "image.h"
#include "image_db.h"
#include "bomb.h"

board_t board[2];
board_t board2[2];
board_t board3[2];
int dbuf=0;
int remap[max_heat]; /* > largest possible mask */
int p1 = 0;

int auto_rule = 0;

/* for colormap changes */
static u_char target_cmap[256][3];
int current_cmap[256 * 3];
#define SET_TARGET_PALETTE(x, r, g, b) \
	target_cmap[x][0] = r; \
	target_cmap[x][1] = g; \
	target_cmap[x][2] = b


cmap_t cmap;
fill_t fill;
rule_t rule;
#define RULEBUFSIZE 10
rule_t rules[RULEBUFSIZE];

image8_t fb;

int masks[max_heat];



void
ramp(int c, int i0, int i1, int n, int k)
{
   int x, r, g, b;
   for (x = 0; x < n; x++) {
      double alpha = x / (double) n;
      r = (the_cmaps[c][i0][0] * (1 - alpha) +
	   the_cmaps[c][i1][0] * alpha);
      g = (the_cmaps[c][i0][1] * (1 - alpha) +
	   the_cmaps[c][i1][1] * alpha);
      b = (the_cmaps[c][i0][2] * (1 - alpha) +
	   the_cmaps[c][i1][2] * alpha);
      SET_TARGET_PALETTE(k + x, r>>2, g>>2, b>>2);
   }
}

int
round_up8(int n)
{
   if (n < 0)
      n = (n - 7) >> 3;
   else
      n = (n + 7) >> 3;
   return n;
}

void
step_cmap()
{
   int i;
   for (i = 0; i < 256; i++) {
      current_cmap[3*i]   += round_up8(target_cmap[i][0] - current_cmap[3*i]);
      current_cmap[3*i+1] += round_up8(target_cmap[i][1] - current_cmap[3*i+1]);
      current_cmap[3*i+2] += round_up8(target_cmap[i][2] - current_cmap[3*i+2]);
   }
#define difff(x,y) ((x)!=(y))
   for (i = 0; i < 256; i++) {
      if (difff(current_cmap[3*i+0], target_cmap[i][0]) ||
	  difff(current_cmap[3*i+1], target_cmap[i][1]) ||
	  difff(current_cmap[3*i+2], target_cmap[i][2])) {
	 vga_setpalvec(0, 256, current_cmap);
	 return;
      }
   }
}

void
set_cmap(cmap_t *p)
{
   int r, g, b, x;
   int c, i, i0, i1, i2;

   switch (p->cmap) {
    case cmap_mono:
      for(x=0;x<256;x++) {
	 r = g = b = x>>2;
	 /* if (0 == (x & 3)) r+=4; */
	 SET_TARGET_PALETTE(x,r,g,b);
      }
      break;
    case cmap_mono4:
      i0 = p->index;
      i1 = p->index2;
      for(x=0;x<256;x++) {
	 if (x == i0) {
	    r = (i1&3)<<4;
	    g = (i1&12)<<2;
	    b = (i1&48);
	 } else
	    r = g = b = x&63;
	 SET_TARGET_PALETTE(x,r,g,b);
      }
      break;
    case cmap_loop:
      c = p->index;
      i0 = R%256;
      i1 = next_contrasting_color(c, i0, 60000);
      i2 = next_contrasting_color(c, i1, 60000);
      ramp(c, i0, i1, 64, 0);
      ramp(c, i1, i2, 64, 64);
      ramp(c, i2, i1, 64, 128);
      ramp(c, i1, i0, 64, 192);
      break;
    case cmap_heat:
      /* black -> midred -> red -> yellow -> white
	 -> = 64 */
      for (x = 0; x < 128; x++) {
	 r = g = 63;
	 b = x >> 1;
	 SET_TARGET_PALETTE(128 + x, r, g, b);
      }
      for (x = 0; x < 64 ;x++) {
	 r = 63;
	 b = 0;
	 g = x;
	 SET_TARGET_PALETTE(64 + x, r, g, b);
      }
      for(x = 0; x < 64; x++) {
	 r = x;
	 g = 0;
	 b = 0;
	 SET_TARGET_PALETTE(x, r, g, b);
      }
      break;
    case cmap_plain:
      i = p->index;
      for (x = 0; x < 256; x++) {
	 r = the_cmaps[i][x][0]>>2;
	 g = the_cmaps[i][x][1]>>2;
	 b = the_cmaps[i][x][2]>>2;
	 SET_TARGET_PALETTE(x,r,g,b);
      }
      break;
    case cmap_split:
      i = p->index;
      for(x=0;x<128;x++) {
	 r = the_cmaps[i][x][0]>>2;
	 g = the_cmaps[i][x][1]>>2;
	 b = the_cmaps[i][x][2]>>2;
	 SET_TARGET_PALETTE(x,r,g,b);
      }
      i = p->index2;
      for(x=128;x<256;x++) {
	 r = the_cmaps[i][x][0]>>2;
	 g = the_cmaps[i][x][1]>>2;
	 b = the_cmaps[i][x][2]>>2;
	 SET_TARGET_PALETTE(x,r,g,b);
      }
      break;
    case cmap_noise:
      for (x = 0; x < 256; x++) {
	 SET_TARGET_PALETTE(x,R,R,R);
      }
      break;
    case cmap_black:
      for (x = 0; x < 256; x++) {
	 SET_TARGET_PALETTE(x,0,0,0);
      }
      break;
   }
}


/* spook */
int
count_bits16(int i)
{
   int b, r = 0;
   for (b = 1; b < 0x10000; b = b << 1)
      if (b & i)
	 r++;
   return r;
}

/* find last set, like ffs */
int
fls(int i)
{
   int r = 1;
   int b = 1;
   while (b < i) {
      r++;
      b = b << 1;
   }
   return r;
}

void
init_masks()
{
   int i, c;
   c = 0;
   i = 1;
   while (c < max_heat) {
      if (8 < count_bits16(i)) {
	 masks[c++] = i;
      }
      i += 2;
   }
}

int
compare_integers(const void *k, const void *a)
{
   return *(int *)a - *(int *)k;
}

/* generally should just keep the index */
int
next_mask(int mask, int d)
{
   int *r;
   int i;
   int s = (d > 0) ? 2 : -2;
   d = abs(d);
   d = (d > 0) ? d : -d;
   while (d) {
      if (8 < count_bits16(mask))
	 d--;
      mask += s;
   }
   return mask;
#if 0
   r = bsearch(&mask, masks, max_heat, sizeof(int), compare_integers);
   if (NULL == r)
      return masks[0];
   i = (r - masks + d) % max_heat;
   return masks[i];
#endif
}

void load_title(char *fn)
{
   int i;
   Image im;

   for (i = 0; i < 256; i++) {
      int *v = &current_cmap[3*i];
      v[0] = v[1] = v[2] = i>>2;
   }
   vga_setpalvec(0, 256, current_cmap);

   image_init(&im);
   image_read(&im, fn);
   image8_blit(&im, &fb);
}

void
init()
{
   int i;
   long t;
   vga_modeinfo *modeinfo;

   t = time(NULL);
   srandom(argd("seed", t));

   vga_init();
   vga_setmode(G320x200x256);
   modeinfo = vga_getmodeinfo(G320x200x256);
   fb.p = vga_getgraphmem();
   fb.stride = modeinfo->linewidth;
   fb.width = 320;
   fb.height = 200;

   load_title("title.gif");

   init_masks();
   init_flames();
   init_images();
   init_seq();
   init_cmaps();

   distrib(distrib_new, &rule, &cmap, &fill);
   rule.drift_time = 0.0;
   drive_with_image(current_image);

   /* start out with basically the same rule */
   rule.rule = rule_rug;
   rule.speed = -1;
   rule.speed_base = -1;
   rule.mask = 255;
   rule.remap = 0;
   rule.randomized_underflow = 1;
   rule.speed_beat_size = 0;
   cmap.cmap = cmap_plain;
   cmap.index = 3%ncmaps;
   set_cmap(&cmap);
   pix2heat(&fb, &board[dbuf]);

   /* i don't use this, but i'm not ready to
      delete it yet */
   if (!getenv("quick"))
      for (i = 0; i < RULEBUFSIZE; i++)
	 distrib(distrib_new, rules + i, 0, 0);

   /* this forces the next several rule changes */
   auto_rule = 3;
}

int
rule_symmetry(int rule)
{
   int r;
   switch (rule) {
    case rule_wave:
    case rule_rug_brain:
    case rule_rug_anneal: 
    case rule_rug_anneal2: r = sym_tile4; break;
    case rule_acidlife2: r = sym_mirror4; break;
    case rule_acidlife1: r = sym_mirror2; break;
    case rule_fuse:
    case rule_slip: r = sym_frame_buffer; break;
    case rule_static: /* false, but useful */
    default:
      r = sym_one; break;
   }
   return r;      
}

void
change_rules(int old, int new, image8_t *fb)
{
   int old_sym = rule_symmetry(old);
   int new_sym = rule_symmetry(new);

   if (new == rule_rug_image && old != new) {
      drive_with_image(current_image);
   }
   if (old_sym == new_sym)
      return;
   if (sym_frame_buffer == old_sym) {
      pix2heat(fb, &board[dbuf]);
      old_sym = sym_one;
   }
   change_rules2(old_sym, new_sym, &board[dbuf]);
   change_rules2(old_sym, new_sym, &board2[dbuf]);
   /* some of the rules don't copy board2
      rug_rug is always going to fail sometimes until
      dbuf/frame&1 is resolved XXX */
   change_rules2(old_sym, new_sym, &board2[1-dbuf]);
}

void
distrib(int dist, rule_t *rule, cmap_t *cmap, fill_t *fill)
{
   int x, b, i, nbits;
   if (rule) {
      static int distrib[] =
      {rule_rug, rule_rug_brain, rule_rotorug, rule_rug_image};
      if (!(distrib_continuous & dist)) {
	 init_rotate();
	 init_shade();
	 if (auto_rule) {
	    rule->rule = distrib[auto_rule%alen(distrib)];
	    auto_rule--;
	 } else if (!(distrib_rule_lock & dist)) {
	    rule->rule = (R%2) ? (R%nrules) : distrib[R%alen(distrib)];
	    if (rule_fuse == rule->rule)
	       rule->rule = rule_rotorug;
	 }
	 rule->cycle_bkg = (0 == R%10);
	 rule->remap = R%4;
	 rule->brain = R;
	 if (rule_rug_multi == rule->rule)
	    rule->brain_shift = R;
	 else
	    rule->brain_shift = (R%4) ? 0 : ((R%3) ? (R%5) : (R%25));
	 random_control_point(&rule->flame_cp);
	 if (auto_rule) {
	    int i, j;
	    static int best[] = {0, 1, 2, 5};
	    int v = best[R%alen(best)];
	    for (i = 0; i < NXFORMS; i++) {
	       for (j = 0; j < NVARS; j++)
		  rule->flame_cp.xform[i].var[j] = 0.0;
	       rule->flame_cp.xform[i].var[v] = 1.0;
	    }
	    
	 }
	 pick_liss_coefs();

	 if (auto_rule) {
	    rule->drift = 4;
	 } else if (rule_fuse == rule->rule)
	    rule->drift = (R%5) ? (R%2) : 3;
	 else
	    rule->drift = R;
	 rule->image_window_size = 2 + R%5;
	 rule->speed_beat_speed = (R%3) ? (5 + R%10) : (30 + R%30);
	 rule->speed_beat_size = (R%3) ? 0 : (R%3) ? (4 + R%12) : (10 + R%22);
	 rule->seq[0] = R;
	 seq_start(rule->seq);
	 p1 = R;
	 current_image = R;
      }
      for (i = 0; i < MAXRHYTHM; i++)
	 rule->rhythm[i] = (R%3 + 1)*8;
      rule->bsize = (R%4) ? (20 + R%20 + R%20) : (60 + R%80);
      rule->randomized_underflow = R&1;
      rule->search_time = 2 + R%5;
      if (R%2) {
	 rule->hot = 200;
	 rule->cool = 5 + R%20 + R%20;
      } else {
	 rule->hot = 1 + 5 * (R%3);
	 rule->cool = 5 + R%20 + R%10 + rule->hot;
      }
      if ((distrib_original & dist) ||
	  rule_acidlife1 == rule->rule ||
	  rule_acidlife2 == rule->rule ||
	  rule_rotorug == rule->rule) {
	 nbits = R%7 + 8;
	 rule->mask = (1<<nbits) - 1;
	 for (b = R%4; b; b--)
	    rule->mask &= ~(2<<(R%nbits));
      } else {
	 nbits = 8 + (R%3 + 1) * (R%2 + 1);
	 rule->mask = (1<<nbits) - 1;
	 for (b = R%3 + 1; b; b--)
	    rule->mask &= ~(2<<(R%nbits));
      }

      switch (rule->rule) {
       case rule_acidlife1:
	 x = -(1 + (rule->mask/100) + R%(rule->mask/30));
	 break;
       case rule_acidlife2:
	 x = -(R%20);
	 break;
       case rule_rotorug:
	 x = -(9 + R%(rule->mask/30));
	 break;
       case rule_wave:
	 x = -(10 + R%10);
	 break;
       default:
	 if (distrib_original & dist)
	    x = -(9 + R%(rule->mask/30));
	 else
	    x = -1;
	 break;
      }
      rule->speed_base = x;
      rule->speed = x;

      if (R%4)
	 rule->driver_slowdown = 1 + R%8 + R%7;
      else
	 rule->driver_slowdown = 13 + R%8;


      rule->drift_speed = 6 + R%10;

      for (x = 0; x < rule->mask; x++) {
	 double base = 10.0;
	 /* remap[x] = 255 * log(1 + (base-1)*(1+x)/(double)rule->mask)/log(base); */
	 remap[x] = 255 * x / (double)rule->mask;
      }
   }
   if (cmap) {
      static distrib[] = {cmap_mono, cmap_mono, cmap_mono4, cmap_heat};
      if (rule && rule->rule == rule_acidlife1)
	 cmap->cmap = cmap_split;
      else if (R%7)
	 cmap->cmap = (R%2) ? cmap_plain : cmap_loop;
      else if (R%100)
	 cmap->cmap = distrib[R%alen(distrib)];
      else
	 cmap->cmap = cmap_noise;

      cmap->index = R%ncmaps;
      cmap->index2 = R%ncmaps;
   }
   if (fill) {
      static distrib[] = {fill_vnoise, fill_noise};
      fill->fill = (R%2) ? (R%nfills) : distrib[R%alen(distrib)];
   }
}


flip_image()
{
   /* Z = zxC-b */ /* at one point, that meant something */
   int im = seq_next_image(rule.seq);
   file_to_image(im, current_image);
   distrib(distrib_new | distrib_continuous, &rule, 0, &fill);
   fill_board(&fill);
}

image_to_heat(Image *src_img, image8_t *fb, board_t *board)
{
   int bsize = rule.bsize;
   int dest_x, dest_y;
   Image src_rect;
   image8_t dest = *fb;
   int move_heat = 1;

   switch (rule_symmetry(rule.rule)) {
    case sym_tile4:
    case sym_mirror4:
      dest_x = R%((fb->width>>1) - bsize);
      dest_y = R%((fb->height>>1) - bsize);
      break;
    case sym_mirror2:
      dest_x = R%((fb->width>>1) - bsize);
      dest_y = R%(fb->height - bsize);
      break;

    case sym_frame_buffer:
      move_heat = 0;
    default:
      dest_x = R%(fb->width - bsize);
      dest_y = R%(fb->height - bsize);
      break;
   }
   image_random_tile(&src_rect, src_img, bsize);
   dest.p = fb->p + fb->stride * dest_y + dest_x;
   dest.width = src_rect.width;
   dest.height = src_rect.height;
   /* could be any operation here, not just copy */
   if (1) {
      int i, j;
      for (i = 0; i < src_rect.height; i++)
	 for (j = 0; j < src_rect.width; j++)
	    dest.p[dest.stride * i + j] =
	       255 - src_rect.pixels[src_rect.stride * i + j];
   }

   if (move_heat)
      pix2heat2(fb, board,
		dest_x, dest_y,
		dest_x + src_rect.width, dest_y + src_rect.height);
}


Image *pick_image(rule_t *p)
{
   Image *r;
   int n = iclamp(R, p->image_window_size);

   switch (p->rule) {
    case rule_fuse:
    case rule_static:
    case rule_slip:
      r = &global_images[n];
      break;
    default:
      r = &global_images_small[n];
      break;
   }
   return r;
}
      

void
changed_gparam(int *gparam)
{
   if (&cmap.index == gparam) {
      cmap.index = iclamp(cmap.index, ncmaps);
      set_cmap(&cmap);
   } else if (&rule.cycle_bkg == gparam ||
	      &rule.remap == gparam ||
	      &rule.randomized_underflow == gparam)
      *gparam = (*gparam)&1;
   else if (&rule.image_window_size == gparam) {
      if (*gparam > N_RAM_IMAGES)
	 *gparam = N_RAM_IMAGES;
      else if (*gparam < 1)
	 *gparam = 1;
   } else if (&rule.seq[0] == gparam) {
      seq_start(rule.seq);
   }

   /* printf("changed gparam %#x to %d\n", gparam, *gparam); */
}

int auto_mode = 0;

extern void step_rule_rug_image(int frame, rule_t *p, image8_t *fb);

void
main() {
   int frame = 0;
   int key;
   int timer = 500;
   int overdrive_speed = 20;
   int dtime = 1;
   int flipper = 1;
   int param = 0;
   int curs = 0;
   int *gparam;
   int old_rule;
   int pulse = 0;
   int delayed_fill = 0;
   int periodic_write = 0;
   int cmap_changed = 0;
   int image_rate = 0;
   int image_dst = 0;
   int rule_lock = 0;

   gparam = &rule.speed;

   init();

   while (1) {
      if (pulse) {
	 pulse = 0;
	 rule.floor = 0;
      } else {
	 rule.floor = (frame % (rule.hot + rule.cool)) > rule.hot;
      }

      if (periodic_write &&
	  0 == (frame % periodic_write))
	 write_fb_ppm(&fb);

      if (auto_mode && run_hist(&fb)) {
	 fill.fill = fill_noise;
	 delayed_fill = 1;
      }

      step_cmap();

      switch (rule.rule) {
       case rule_rug:
	 step_rule_rug(frame, &rule, &fb);
	 break;
       case rule_rug2:
	 step_rule_rug2(frame, &rule, &fb);
	 break;
       case rule_static:
	 step_rule_static(frame, &rule, &fb);
	 break;
       case rule_rotorug:
	 step_rule_rotorug(frame, &rule, &fb);
	 break;
       case rule_acidlife1:
	 step_rule_acidlife1(frame, &rule, &fb);
	 break;
       case rule_acidlife2:
	 step_rule_acidlife2(frame, &rule, &fb);
	 break;
       case rule_rug_anneal:
	 step_rule_rug_anneal(frame, &rule, &fb);
	 break;
       case rule_rug_anneal2:
	 step_rule_rug_anneal2(frame, &rule, &fb);
	 break;
       case rule_rug_rug:
	 step_rule_rug_rug(frame, &rule, &fb);
	 break;
       case rule_rug_brain:
	 step_rule_rug_brain(frame, &rule, &fb);
	 break;
       case rule_shade:
	 step_rule_shade(frame, &rule, &fb);
	 break;
       case rule_wave:
	 step_rule_wave(frame, &rule, &fb);
	 break;
       case rule_rug_image:
	 step_rule_rug_image(frame, &rule, &fb);
	 break;
       case rule_slip:
	 step_rule_slip(frame, &rule, &fb);
	 break;
       case rule_fuse:
	 step_rule_fuse(frame, &rule, &fb);
	 break;
       case rule_rug_multi:
	 step_rule_rug_multi(frame, &rule, &fb);
	 break;
      }

      frame++;
      timer = timer - dtime;
      old_rule = rule.rule;

      if (rule.speed_beat_speed != 0)
	 rule.speed = rule.speed_base +
	    0.5 * rule.speed_beat_size *
	       sin(2.0 * M_PI * frame / rule.speed_beat_speed);
      else
	 rule.speed = rule.speed_base;

      if ((key = vga_getkey()) > 0) {
	 timer = 10000;
	 auto_mode = 0;
	 switch (key) {
	    /* boy this is really dumb */
	  case '0': case '1': case '2': case '3': case '4':
	  case '5': case '6': case '7': case '8': case '9':
	    param = 10 * param + (key - '0');
	    break;
	  case '\n':
	    *gparam = param;
	    param = 0;
	    changed_gparam(gparam);
	    break;
	  case '.':
	  incr:
	    (*gparam)++;
	    changed_gparam(gparam);
	    break;
	  case ',':
	    (*gparam)--;
	    changed_gparam(gparam);
	    break;

#define dogparm(field) \
  if (gparam == &(field)) goto incr; gparam = &(field); break;

	  case 'q': dogparm(cmap.index);
	  case 'w': dogparm(rule.speed_beat_size);
	  case 'e': dogparm(rule.speed_beat_speed);
	  case 'r': dogparm(rule.speed_base);
	  case 't': dogparm(rule.image_window_size);
	  case 'y': dogparm(rule.seq[0]);
	  case 'u': dogparm(rule.remap);
	    /* should skip patterns without enough bits */
	  case 'i': dogparm(rule.mask);
	  case 'o': dogparm(p1); break;
	  case 'p': dogparm(rule.bsize);
	  case '[': dogparm(rule.drift_speed);
	  case ']': dogparm(rule.driver_slowdown);
	  case '\\':dogparm(rule.search_time);
	  case '{': dogparm(rule.hot);
	  case '}': dogparm(rule.cool);
	  case '|': dogparm(rule.brain);
	  case 'P': dogparm(rule.brain_shift);
	  case 'O': dogparm(rule.cycle_bkg);
	  case 'I': dogparm(rule.randomized_underflow);


	  case 'a':
	    if (rule_rug == rule.rule)
	       rule.rule = rule_rug2;
	    else 
	       rule.rule = rule_rug;
	    break;
	  case 's':
	    rule.rule = rule_rug_image;
	    break;
	  case 'd':
	    rule.rule = rule_static;
	    break;
	  case 'f':
	    if (rule.rule == rule_rotorug)
	       rule.drift++;
	    else
	       rule.rule = rule_rotorug;
	    break;
	  case 'g':
	    if (rule.rule == rule_acidlife2)
	       rule.rule = rule_acidlife1;
	    else
	       rule.rule = rule_acidlife2;
	    break;
	  case 'h':
	    rule.rule = rule_rug_multi;
	    break;
	  case 'j':
	    if (rule.rule == rule_rug_anneal)
	       rule.rule = rule_rug_anneal2;
	    else
	       rule.rule = rule_rug_anneal;
	    break;
	  case 'k':
	    if (rule.rule == rule_slip)
	       rule.drift++;
	    else
	       rule.rule = rule_slip;
	    break;
	  case 'l':
	    rule.rule = rule_rug_rug;
	    break;
	  case ';':
	    rule.rule = rule_rug_brain;
	    break;
	  case '\'':
	    if (rule.rule == rule_shade)
	       rule.rule = rule_wave;
	    else
	       rule.rule = rule_shade;
	    break;

	  case 'F':
	    if (rule.rule == rule_fuse)
	       rule.drift++;
	    else
	       rule.rule = rule_fuse;
	    break;
	  case 'L':
	    rule_lock = rule_lock ? 0 : distrib_rule_lock;
	    break;

	  case ' ':
	    auto_rule = 0;
	    distrib(distrib_new | rule_lock, &rule, &cmap, &fill);
	    set_cmap(&cmap);
	    delayed_fill = 1;
	    image_rate = 0; /* should be in rule struct */
	    timer = 400;
	    break;

	  case 'z':
	    distrib(distrib_new | distrib_continuous, &rule, 0, 0);
	    break;
	  case 'x':
	    distrib(distrib_new, 0, 0, &fill);
	    fill_board(&fill);
	    break;
	  case 'c':
	    distrib(distrib_new, 0, &cmap, 0);
	    if (rule.rule == rule_acidlife1)
	       cmap.cmap = cmap_split;
	    else if (rule.rule == rule_fuse)
	       cmap.cmap = cmap_mono; 
	    set_cmap(&cmap);
	    break;
	  case 'v':
	    switch (rule.rule) {
	     case rule_rug:
	     case rule_rug2:
	       rule.floor = 0;
	       rule.cool = 999999; /* XXX */
	       pulse = 1;
	       break;
	     case rule_rug_image:
	       current_image = iclamp(current_image + 1,
				      rule.image_window_size);
	       drive_with_image(current_image);
	       break;
	     case rule_rotorug:
	       if (1) {
		  int i;
		  random_control_point(&rule.flame_cp);
		  pick_liss_coefs();
		  init_rotate();
		  for (i = 0; i < MAXRHYTHM; i++)
		     rule.rhythm[i] = (R%3 + 1)*8;
	       }
	       break;
	     case rule_rug_brain:
	       rule.brain++;
	       break;
	     case rule_fuse:
	       rotate_images();
	       file_to_image(seq_next_image(rule.seq), 0);
	       break;
	    }
	    break;
	  case 'b':
	    switch (rule.rule) {
	     case rule_rug_brain:
	       rule.brain_shift = rule.brain_shift ? 0 : (R%7);
	       break;
	     case rule_rug_multi:
	       rule.brain_shift++;
	       break;
	     default:
	       file_to_image(p1++, current_image);
	       drive_with_image(current_image);
	       break;
	    }
	    break;
	  case 'n':
	    rule.speed_beat_size = rule.speed_beat_size ? 0 : (R%22);
	    rule.speed_beat_speed = (R%3) ? (5 + R%10) : (30 + R%30);
	    break;

	  case 27:
	    load_title("help.gif");
	    while (vga_getkey() <= 0)
	       sleep(1);
	    break;
	    
	  case '/':
	    drive_with_image(current_image);
	    break;
	  case '?':
	    file_to_image(p1, current_image);
	    drive_with_image(current_image);
	    break;
	  case '-':
	    *gparam = - (*gparam);
	    changed_gparam(gparam);
	    break;
	  case '=':
	    write_fb_ppm(&fb);
	    break;
	  case '+':
	    periodic_write = 10 - periodic_write;
	    break;
	  case 'Z':
	    flip_image();
	    break;
	  case 'X':
	    /* X = zcm */
	    distrib(distrib_new | distrib_continuous, &rule, &cmap, 0);
	    init_rotate();
	    if (rule.rule == rule_acidlife1)
	       cmap.cmap = cmap_split;
	    set_cmap(&cmap);
	    random_control_point(&rule.flame_cp);
	    pick_liss_coefs();
	    break;
	  case 'Q':
	    timer = 10;
	    break;
	  case 'W':
	    if (1) {
	       int i;
	       for (i = 1; i < 10; i++) {
		  current_cmap[3*i+0] = ((current_cmap[3*i+0] * (10 - i) / 10) +
					 (63 * i / 10));
		  current_cmap[3*i+1] = ((current_cmap[3*i+1] * (10 - i) / 10) +
					 (63 * i / 10));
		  current_cmap[3*i+2] = ((current_cmap[3*i+2] * (10 - i) / 10) +
					 (63 * i / 10));
	       }
	       for (i = 10; i < 20; i++) {
		  current_cmap[3*i+0] = 63;
		  current_cmap[3*i+1] = 63;
		  current_cmap[3*i+2] = 63;
	       }
	       for (i = 20; i < 30; i++) {
		  current_cmap[3*i+0] = ((current_cmap[3*i+0] * (i - 20) / 10) +
					 (63 * (30 - i) / 10));
		  current_cmap[3*i+1] = ((current_cmap[3*i+1] * (i - 20) / 10) +
					 (63 * (30 - i) / 10));
		  current_cmap[3*i+2] = ((current_cmap[3*i+2] * (i - 20) / 10) +
					 (63 * (30 - i) / 10));
	       }
	    }
	    break;
	  case 'E':
	    if (1) {
	       int i;
	       for (i = 100; i < 150; i++) {
		  current_cmap[3*i+0] = current_cmap[3*i+0] * (150 - i) / 50;
		  current_cmap[3*i+1] = current_cmap[3*i+1] * (150 - i) / 50;
		  current_cmap[3*i+2] = current_cmap[3*i+2] * (150 - i) / 50;
	       }
	       for (i = 150; i < 250; i++) {
		  current_cmap[3*i+0] = 0;
		  current_cmap[3*i+1] = 0;
		  current_cmap[3*i+2] = 0;
	       }
	    }
	    break;
	  case 'R':
	    if (1) {
	       int i;
	       for (i = 1; i < 220; i++) {
		  current_cmap[3*i+0] = current_cmap[3*i+0+90];
		  current_cmap[3*i+1] = current_cmap[3*i+0+91];
		  current_cmap[3*i+2] = current_cmap[3*i+0+92];
	       }
	    }
	    break;
	  case 'T':
	    if (1) {
	       int i;
	       for (i = 0; i < 256; i++) {
		  int r = (63 + 4*current_cmap[3*i+0])/5;
		  int g = (63 + 4*current_cmap[3*i+1])/5;
		  int b = (63 + 4*current_cmap[3*i+2])/5;
		  SET_TARGET_PALETTE(i, r, g, b);
	       }
	    }
	    break;
	  case 'V':
	    rule.drift--;
	    break;
	  case 'C':
	    invert_board();
	    break;
	  case 'Y':
	    cool(&fb);
	    break;
	  case 'U':
	    warm(&fb);
	    break;
	  case 2: /* ^B */
	    rotate_images();
	    file_to_image(R, current_image);
	    drive_with_image(current_image);
	    break;
	  case 15: /* ^O */
	    rotate_images();
	    file_to_image(R, 0);
	    break;
	  case 26: /* ^Z */
	    random_image_set();
	    break;
	  case 'N':
	    rule = rules[curs];
	    if (curs == 0) curs = RULEBUFSIZE;
	    curs--;
	    break;
	  case 14: /* ^N */
	    rule = rules[curs];
	    if (++curs == RULEBUFSIZE) curs = 0;
	    break;
	  case 'M':
	    rule.flame_cp = flames[iclamp(p1, nflames)];
	    break;
	  case 9: /* ^I */
	    image_to_heat(pick_image(&rule), &fb, &board[dbuf]);
	    image_dst = 1;
	    break;
	  case 21: /* ^U */
	    image_to_heat(pick_image(&rule), &fb, &board2[dbuf]);
	    image_dst = 0;
	    break;
	  case 25: /* ^Y */
	    image_rate = (image_rate+1)&0x3;
	    break;
	  case 'B':
	    cmap.cmap = cmap_black;
	    set_cmap(&cmap);
	    break;
	  case 18: /* ^R */
	    dtime++;
	    if (dtime == 8)
	       dtime = 1;
	    break;
	    
	  default:
	    printf("unknown key = %d\n", key);
	    break;
	 }
      }

      if (rule.drift)
	 rule.drift_time += (double) rule.drift_speed;

      flipper = (frame >> 9) & 0x3;

      if (0 >= timer) {
	 auto_mode = 1;
	 if (flipper && (rule_rug_brain == rule.rule) && (R%10)) {
	    switch (flipper) {
	     case 1:
	       rule.brain++;
	       break;
	     case 2:
	       distrib(distrib_new|distrib_continuous, &rule, 0, 0);
	       break;
	     case 3:
	       rule.brain_shift =
		  (R%8) ? (rule.brain_shift + ((R&1) ? -1 : 1)) : 0;
	       break;
	    }
	    timer = 50;
	 } else if (flipper && (rule_rug_image == rule.rule) && (R%5)) {
	    static here = 0;
	    /* this creates an echo effect */
	    if (here = 1-here) {
	       current_image = iclamp(current_image + 1,
				      rule.image_window_size);
	       drive_with_image(current_image);	       
	    } else if (flipper&1) {
	       int im = seq_next_image(rule.seq);
	       file_to_image(im, current_image);
	       drive_with_image(current_image);
	    } else
	       flip_image();
	    timer = 100;
	 } else if (flipper && (rule_fuse == rule.rule) && (R%10)) {
	    rotate_images();
	    switch (flipper) {
	     case 1:
	       file_to_image(p1++, 0);
	       break;
	     case 2:
	       file_to_image(R, 0);
	       break;
	    }
	 } else if ((3 == iclamp(rule.drift, 4)) &&
		    (rule_rotorug == rule.rule) && (R%5)) {
	    timer = 100;
	    if (R&1) {
	       p1 = R;
	       rule.flame_cp = flames[iclamp(p1, nflames)];
	    } else {
	       random_control_point(&rule.flame_cp);
	    }
	 } else if (flipper && (rule_rug_rug == rule.rule) && (R%5)) {
	    distrib(distrib_new|distrib_continuous, &rule, 0, 0);
	    timer = 200;
	 } else if (R%2 && (frame - cmap_changed) > 200) {
	    /* ugh */
	    if (rule.rule == rule_acidlife1) {
	       cmap.cmap = cmap_split;
	       set_cmap(&cmap);
	    } else if (rule.rule == rule_fuse && R%4) {
	       cmap.cmap = cmap_mono;
	       set_cmap(&cmap);
	    } else {
	       distrib(distrib_new, 0, &cmap, 0);
	       set_cmap(&cmap);
	    }
	    timer = 300;
	    cmap_changed = frame;
	 } else if (R%4) {
	    int t = 0;
	    /* this is the same condition as used for overdrive */
	    if (3 == flipper)
	       t = distrib_rule_lock;
	    distrib(distrib_new | t, &rule, 0, 0);
	    timer = 300;
	    /* ugh */
	    if (rule.rule == rule_acidlife1) {
	       cmap.cmap = cmap_split;
	       set_cmap(&cmap);
	    }
	 } else {
	    distrib(distrib_new, 0, 0, &fill);
	    fill_board(&fill);
	    timer = 300;
	 }
	 flipper = (frame >> 7) & 0x3;
	 if (!(R%20))
	    overdrive_speed = 5 + R%10 + R%20;
	 if (3 == flipper && frame > 2000)
	    timer = overdrive_speed;

      }


      if (auto_mode && !(R%500)) image_dst = !image_dst;
      if (auto_mode && !(frame%1000)) {
	 rule.seq[0] = R;
	 seq_start(rule.seq);
      }

      /* i really don't know how much i like this stuff */
      if (1) {
	 int tween;
	 static int tab1[] = {300, 200, 100, 50};
	 static int tab2[] = {10000, 200, 100, 50};
	 if (auto_mode)
	    switch (rule.rule) {
	     case rule_slip:
	     case rule_fuse:
	       if (!(R%tab1[image_rate]))
		  image_rate = R&3;
	       break;
	     default:
	       if (frame < 10000)
		  image_rate = 0;
	       else if (!(R%tab2[image_rate]))
		  image_rate = R&3;
	       break;
	    }
	 if (image_rate) {
	    tween = 1 << ((3-image_rate) * 3);
	    if (!(frame%tween)) {
	       if (image_dst)
		  image_to_heat(pick_image(&rule), &fb, &board[dbuf]);
	       else
		  image_to_heat(pick_image(&rule), &fb, &board2[dbuf]);
	    }
	 }
      }

      change_rules(old_rule, rule.rule, &fb);

      if (delayed_fill) {
	 fill_board(&fill);
	 delayed_fill = 0;
      }
   }
}
