/*

   The Wreck of the Steamer "Stella" - client-side runtime and support
   -------------------------------------------------------------------

   This code is used to aid client-side execution of compiled WSS code.

   Copyright (C) 2007 by Michal Zalewski <lcamtuf@coredump.cx>

*/

/********************/
/* Runtime settings */
/********************/

var LINES_ONCE = 64;		/* Timeslice greed. More improves
                                   performance very significantly,
                                   but makes browser more vulnerable
                                   to malicious busy loops. */

var USE_TTL = true;		/* Use jump expiration? */

/************************************************************/
/* Language parameters - must be in sync with the compiler! */
/************************************************************/

var MAX_X      = 32;		/* Screen X resolution       */
var MAX_Y      = 16;		/* Screen Y resolution       */
var MAX_LABELS = 8;		/* Label count limit         */
var MAX_MEM    = 16;		/* Memory size limit         */
var MAX_VALUE  = 64;		/* Memory value limit        */
var LINE_TTL   = 64;		/* Line lifetime max         */

var CHARSET =
  [ '\n', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
     'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
     'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 
     'Y', 'Z', '.', ',','\'', '"', '?', '!', '-', '+', '*', '/', 
     '=', '%', '<', '>', ':', ';', '|', '_', '@', '#', '$', '&',
     '(', ')', '[', ']' ];

var COLORS = 
  [ 'darkgray', 'red', 'green', 'blue', 
    'cyan', 'purple', 'yellow', 'white' ];

/* Code proper */

var line_code;			/* Lines of code to execute  */
var line_ttl;			/* Line expiration clocks    */
var labels; 		 	/* Call label line numbers   */

var screen_char;		/* Screen data (chars)       */
var screen_color;		/* Screen data (fg colors)   */

var curX;			/* current screen X          */
var curY;			/* current screen Y          */
var curCol;			/* current screen color      */

var memory;			/* Process memory            */
var read_ptr;			/* Memory read head          */
var write_ptr;			/* Memory write head         */

var cur_line;			/* Current execution pointer */
var cycle_count; 		/* Number of cycles thus far */
var key_wait = false;		/* Waiting for keypress?     */

var code_timer   = false;
var update_timer = false;
var initialized  = false;
var run_complete = false;
var ignore_keys  = false;
var cursor_state = true;
var screen_dirty = false;
var key_timer    = false;

var loaded_program;		/* Loaded program data.      */

/*********************
 * UI callbacks, etc *
 *********************/

/* (Re-)initialize program to prepare for running (will abort a running program). */
function wss_initialize() {

  if (code_timer) {
    clearInterval(code_timer);
    code_timer = false;
  }

  if (key_timer) {
    clearInterval(key_timer);
    key_timer = false;
  }

  initialized  = true;
  run_complete = false;

  line_code    = new Array(0);
  line_ttl     = new Array(0);
  labels       = new Array(MAX_LABELS);
  screen_char  = new Array(MAX_X * MAX_Y);
  screen_color = new Array(MAX_X * MAX_Y);
  curX         = 0;
  curY         = 0;
  curCol       = 7;
  memory       = new Array(MAX_MEM);
  read_ptr     = 0;
  write_ptr    = 0;
  cur_line     = 0;
  cycle_count  = 0;

  key_wait     = false;
  ignore_keys  = false;

  document.getElementById('key_prompt').style.visibility = 'hidden';

  /* Initialize memory. */
  for (i=0;i<MAX_MEM;i++) memory[i] = 0;

  eval(loaded_program);

  /* Initialize TTLs. */
  for (i=0;i<line_code.length;i++) 
    line_ttl[i] = LINE_TTL;

}


/* 'Load' key handler. */
function wss_load() {
  /* Keep form data - why not. */
  document.getElementById('load_screen').style.display = 'block';
  document.getElementById('text_input').focus();
}


/* Load cancel handler. */
function wss_load_cancel() {
  document.getElementById('load_screen').style.display = 'none';
}


/* Load operation handler. */
function wss_load_do() {
  var input = document.getElementById('text_input').value;
  var key = document.getElementById('text_key').value;

  try {

    var x = new XMLHttpRequest();
    x.open('POST','wss-compile.cgi?' + escape(key), false);
    x.send(input);
    if (x.status != 200) throw 1;

    loaded_program = x.responseText;

    document.getElementById('run_button').disabled = false;
    document.getElementById('source_button').disabled = false;

    wss_initialize();
    wss_redraw_screen();
    wss_update_status();

    /* Script should return at least one line. */
    if (!line_code.length) throw 1;

    alert('Program loaded successfully (' + (line_code.length - 1) + ' instructions).');

  } catch (e) {
    alert('Unable to request server-side compilation, sorry.');
    return;
  }

  document.getElementById('load_screen').style.display = 'none';
}

/* View source function handler */
function wss_source() {
  document.getElementById('source_code').value = loaded_program;
  document.getElementById('view_source').style.display = 'block';
  document.getElementById('source_cancel').focus();
}


/* View source 'close' handler. */
function wss_source_cancel() {
  document.getElementById('view_source').style.display = 'none';
}



/* Run non-running program, initialize as needed. */
function wss_run() {
  if (!initialized || run_complete) wss_initialize();

  document.getElementById('load_button').disabled = true;
  document.getElementById('run_button').disabled = true;
  document.getElementById('stop_button').disabled = false;

  code_timer = setInterval('wss_next_line()',1);
  update_timer = setInterval('wss_update_status()',100);

}


/* Execute next line (called from timer). */
function wss_next_line() {
  var tmp_line;

  for (cnt=0;cnt<LINES_ONCE;cnt++) {

    if (key_wait) return;

    /* I would much rather not use eval for performance reasons,
       but Javascript developers believe I would be too reckless
       having a working GOTO statement, oh yeah. */

    if (cur_line >= line_code.length) {

      if (code_timer) {
        clearInterval(code_timer);
        code_timer = false;
      }

      if (screen_dirty)
        wss_redraw_screen();

      run_complete = true;
      return;

    }

    cycle_count++;

    /* We pre-increment, so that jumps work properly */

    tmp_line = cur_line;
    cur_line++;

    if (line_ttl[tmp_line] == 0) continue; /* NOP */

    eval(line_code[tmp_line]);

    /* Expire successful calls is USE_TTL is in effect. */
    if (USE_TTL && cur_line - 1 != tmp_line) line_ttl[tmp_line]--;

  }

  if (screen_dirty)
    wss_redraw_screen();

}


/* Update screen data in main HTML document. */
function wss_redraw_screen() {
  var display_string = '';
  var pcol = false;

  for (y=0;y<MAX_Y;y++) {
    for (x=0;x<MAX_X;x++) {

      if (x == curX && y == curY) 
        display_string += '<span id="cursor">';

      if (screen_char[x + y * MAX_X]) {

        if (pcol != screen_color[x + y * MAX_X]) {
          if (pcol) display_string += '</font>';
          pcol = screen_color[x + y * MAX_X];
          display_string += '<font color=' + pcol + '>';
        }

        display_string += screen_char[x + y * MAX_X];

      } else 
        display_string += '&nbsp;';

      if (x == curX && y == curY) 
        display_string += '</span>';

    }

    if (y != MAX_Y - 1) display_string += '<br>';

  }

  if (pcol) display_string += '</font>';

  document.getElementById('screen').innerHTML = display_string;
  screen_dirty = false;

}


/* Update status bar in main HTML document. */
function wss_update_status() {
  if (!code_timer) {

    document.getElementById('exec_status').innerHTML = '<font color="#FF8010">STOPPED</font>';

    if (update_timer) {
      clearInterval(update_timer);
      update_timer = false;
    }

    document.getElementById('run_button').disabled = false;
    document.getElementById('stop_button').disabled = true;
    document.getElementById('load_button').disabled = false;

  } else {

    document.getElementById('exec_status').innerHTML = '<font color="#00FF00">RUNNING</font>';

  }

  document.getElementById('exec_cycles').innerHTML = cycle_count;
  document.getElementById('exec_line').innerHTML = cur_line + ' / ' + line_code.length;
  document.getElementById('exec_coords').innerHTML = curX + ' x ' + curY;
  document.getElementById('exec_heads').innerHTML = 'R' + read_ptr + ' W' + write_ptr;

}


/* Cursor blink callback, installed on main page. */
function wss_animate_cursor() {

  if (cursor_state) {
    document.getElementById('cursor').style.backgroundColor = 'black';
  } else {
    document.getElementById('cursor').style.backgroundColor = 'teal';
  }
  cursor_state = !cursor_state;

}


/* Array.indexOf prototype for Firefox compatibility (taken from Mozilla dev center) */
if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function(elt) {
    var len = this.length;

    for (from = 0; from < len; from++) {
      if (from in this && this[from] === elt)
        return from;
    }
    return -1;
  };
}


/* Keypress handler. */
function wss_keypress(e) {
  var key, knum, val;

  if (!key_wait) return true;

  if (key_timer) {
    clearInterval(key_timer);
    key_timer = false;
  }

  if (window.event) knum = window.event.keyCode;
  else if (e.which) knum = e.which;

  key = String.fromCharCode(knum);

  if (key == '\r') key = '\n';

  if (key == '\033' || key == '\000') {
    key_wait = false;
    ignore_keys = true;
    document.getElementById('key_prompt').style.visibility = 'hidden';
    return false;
  }

  val = CHARSET.indexOf(key.toUpperCase());

  if (val != -1) {
    key_wait = false;
    memory[write_ptr] = val;
    document.getElementById('key_prompt').style.visibility = 'hidden';
    return false;
  }

  return true;

}


/* Key wait timeout handler. */
function wss_key_abort() {

  clearInterval(key_timer);
  key_timer = false;

  key_wait = false;
  document.getElementById('key_prompt').style.visibility = 'hidden';

}


/***************************
 * Language implementation *
 ***************************/

/* Do nothing! */
function OP_NOP() { }


/* Swap pointers. */
function OP_SWAP() {
  var tmp = read_ptr;
  read_ptr = write_ptr;
  write_ptr = tmp;
}


/* Move read pointer left. */
function OP_RDL() {
  read_ptr = (MAX_MEM + read_ptr - 1) % MAX_MEM;
}


/* Move read pointer right. */
function OP_RDR() {
  read_ptr = (read_ptr + 1) % MAX_MEM;
}


/* Move write pointer left. */
function OP_WRL() {
  write_ptr = (MAX_MEM + write_ptr - 1) % MAX_MEM;
}


/* Move write pointer right. */
function OP_WRR() {
  write_ptr = (write_ptr + 1) % MAX_MEM;
}


/* Zero wptr memory cell. */
function OP_ZERO() {
  memory[write_ptr] = 0;
}


/* Add immediate value to wptr memory cell. */
function OP_ADD(value) {
  memory[write_ptr] = (memory[write_ptr] + value) % MAX_VALUE;
}


/* Add rptr memory cell to wptr memory cell. */
function OP_ADD_M() {
  OP_ADD(memory[read_ptr]);
}


/* Set call label here. */
function OP_LABEL(lab) {
  /* This is 1 past LABEL opcode */
  labels[lab] = cur_line; 
}


/* Jump to call label. */
function OP_JUMP(lab) {
  if (labels[lab] == undefined) return;
  cur_line = labels[lab];
}


/* Jump to label if rptr cell = wptr cell. */
function OP_JE(lab) {
  if (memory[write_ptr] == memory[read_ptr]) OP_JUMP(lab);
}


/* Jump to label if rptr cell > wptr cell. */
function OP_JA(lab) {
  if (memory[write_ptr] < memory[read_ptr]) OP_JUMP(lab);
}


/* Read key to wptr. */
function OP_READ() {
  if (ignore_keys) return;

  document.getElementById('key_prompt').style.visibility = 'visible';
  key_wait = true;
  document.getElementById('run_button').blur();

  if (memory[read_ptr]) 
    key_timer = setTimeout('wss_key_abort()', memory[read_ptr] * 100);

  if (screen_dirty)
    wss_redraw_screen();

}


/* Write char value to screen. */
function OP_WRITE(char) {

  screen_dirty = true;

  if (char == 0) {

    /* Special case for newline */

    curX = 0;
    curY = (curY + 1 ) % MAX_Y;

  } else {

    var use_char = CHARSET[char];

    switch (use_char) {
      case ' ': use_char = 0; break; 
      case '<': use_char = '&lt;'; break;
      case '>': use_char = '&gt;'; break;
      case '&': use_char = '&amp;'; break;
    }

    screen_color[curX + curY * MAX_X] = COLORS[curCol];
    screen_char[curX + curY * MAX_X] = use_char;

    curX++;
    if (curX == MAX_X) {
      curX = 0;
      curY = ( curY + 1 ) % MAX_Y;
    }

  }

  wss_redraw_screen();

}


/* Write memory value to screen. */
function OP_WRITE_M() {
  OP_WRITE(memory[read_ptr]);
}


/* Set X coordinate to value. */
function OP_SETX(value) {
  if (value != curX) {
    curX = value;
    screen_dirty = true;
  }
}


/* Set Y coordinate to value. */
function OP_SETY(value) {
  if (value != curY) {
    curY = value;
    screen_dirty = true;
  }
}


/* Set screen color to vaue. */
function OP_SETCOL(col) {
  curCol = col;
}


