// JavaScript calculator.  

// Depends on MochiKit.DOM, MochiKit.Base, MochiKit.Iter,
// MochiKit.Signal, and uses MochiKit.Logging --- 4635 lines of
// external JavaScript in all, or 133609 bytes (gzipping to 26162).
// This JavaScript and HTML is 655 LoC, 21336 butes, gzipping to 8479.

///////// Debugging stuff

// dom node containing table displaying object properties
function domdump(obj) {
  var align = {valign: 'top'}
  return TABLE(null, map(function(prop) { 
      return TR(null, TD(align, repr(prop)), TD(align, repr(obj[prop])))
  },
                         keys(obj)))
}

// subset of an object
function slice(obj, props) {
  var rv = {}
  for (var i = 0; i < props.length; i++) rv[props[i]] = obj[props[i]]
  return rv
}

// display relevant properties of an event
function debug_output(key) {
  var out = $('debug_output')
  if (!out) return
  replaceChildNodes(out, domdump(key))
  //out.appendChild(domdump(slice(key, ['isChar', 'keyCode', 'charCode', 
  //                                    'shiftKey', 'type'])))
}

///////// Calculator primitive stuff
// At first it seemed like a good idea to just use DOM nodes for the
// calculator's stack; now that each stack item has three attributes,
// it seems like a less good idea.

// return the value from the top of the stack (can't fail)
function pop_stack() {
  var tos = $('output').lastChild
  if (!tos) return {value: '0', expr: 0, atomic: 1}      // crude hack
  var rv_value = tos.firstChild.nodeValue
  var rv_expr = tos.firstChild.nextSibling.nextSibling.firstChild.nodeValue
  var atomic = tos.getAttribute('atomic')
  $('output').removeChild(tos)
  return {value: rv_value, expr: rv_expr, atomic: atomic}
}

function discard_value() {
  var tos = $('output').lastChild
  if (!tos) return
  $('output').removeChild(tos)
  $('rstack').appendChild(tos)
}

function go_down() {
  var tors = $('rstack').firstChild
  if (!tors) return
  $('rstack').removeChild(tors)
  $('output').appendChild(tors)
}

function drag_down() {
  var tors = $('rstack').firstChild
  if (!tors) return
  var tos = $('output').lastChild
  if (!tos) return go_down()
  $('rstack').removeChild(tors)
  $('output').insertBefore(tors, $('output').lastChild)
}  

function go_up() {
  var tos = $('output').lastChild
  if (!tos) return
  $('output').removeChild(tos)
  $('rstack').insertBefore(tos, $('rstack').firstChild)
}

function drag_up() {
  var tos = $('output').lastChild
  if (!tos) return
  var next = tos.previousSibling
  if (!next) return go_up()
  $('output').removeChild(next)
  $('rstack').insertBefore(next, $('rstack').firstChild)
}
  

function isFinite(number) {
  if (isNaN(number)) return false
  if (number == Infinity) return false
  if (number == -Infinity) return false
  return true
}

// put a graph of an array into a node
function create_graph(val, node) {
  var w = 200
  var h = 20
  var canvas = CANVAS({width: w, height: h})
  if (!canvas.getContext) return  // no canvas support!
  node.appendChild(canvas)
  canvas.data = val
  redraw_graph(canvas)

  connect(canvas, 'onmouseover', mouseover_display_resize_icon)
  connect(canvas, 'onmouseout', mouseout_hide_resize_icon)
}

function redraw_graph(canvas) {
  var val = canvas.data
  var ctx = canvas.getContext('2d')
  var w = canvas.width
  var h = canvas.height

  // determine coordinate transformation
  var ww = w + 1
  var hh = h + 1
  var per_sample = ww / val.length
  var minval = listMin(concat([0], filter(isFinite, val)))
  var maxval = listMax(concat([1], filter(isFinite, val)))
  var size = maxval - minval
  var vertical_xform = function(datum) {
    // maybe I should have the canvas do this?
    return h - (datum - minval) * h / size
  }
  // x-axis
  ctx.strokeStyle = 'grey'
  ctx.moveTo(0, vertical_xform(0))
  ctx.lineTo(ww, vertical_xform(0))

  // plot points
  ctx.strokeStyle = 'black'
  var lastPoint = false
  forEach(range(val.length), function(ii) {
    if (!isFinite(val[ii])) {
      lastPoint = false
      return
    }
    var x = per_sample * (ii + 0.5)
    var y = vertical_xform(val[ii])
    if (lastPoint) {
      ctx.lineTo(x, y)
    } else {
      ctx.moveTo(x, y)
    }
    lastPoint = true
  })
  ctx.stroke()
}

// push a value
function push_stack(val) {
  var n = DIV(null, val.value, SPAN(' = '), SPAN({'class': 'expr'}, val.expr))
  var tograph = asarray('' + val.value)
  if (tograph.length > 1) create_graph(tograph, n)
  if (val.atomic) n.setAttribute('atomic', 1)
  $('output').appendChild(n)
}

///////// Calculator user operations

// Split apart an expression into its "predecessors".  Being really
// cheap-ass and storing all historical values at the moment:
var predecessors = ({})
function split_value() {
  var value = pop_stack()
  //debug_output(predecessors)
  var preds = predecessors[value.expr]
  if (preds == null) {
    // Maybe we should yank stuff off the bottom of the lower stack instead?
    push_stack_with_preds([value], {value: value.expr + ' is atomic',
      expr: 'split ' + get_expr(value)})
    return
  }
  forEach(preds, push_stack)
}

function push_stack_with_preds(preds, val) {
  predecessors[val.expr] = preds
  push_stack(val)
}

// before you invoke any operation, or when you hit Enter, push
// current text field onto stack (if any)
function append_output() {
  // "atomic" means no parens are ever needed around this expression
  var input = $('the_input')
  if (strip(input.value) == '') return
  push_stack({value: input.value, expr: input.value, atomic: 1})
  $('the_input').value = ''
  return false
}

// is the input field empty?
function empty_field() {
  return (strip($('the_input').value) == '')
}

function handle_backspace() {
  if (empty_field()) {
    discard_value()
    return false
  } else return true
}

// get properly wrapped value
function get_expr(obj) {
  if (obj.atomic) return obj.expr
  return "(" + obj.expr + ")"
}

function asarray(astring) {
  // if (!astring.indexOf) alert(astring)
  astring = strip(astring)
  // I was using new Number(astr) here, but it turns out that
  // new Number('45') != new Number('45') and that was causing
  // problems when trying to compute the max or min of a vector.
  return map(function(astr){return parseFloat(astr)},
             astring.split(/\s+/))
}

function bin_apply_fun(fun, y, x) {
  var yy = asarray(y.value)
  if (x.expr == 'reduce') {
    return reduce(fun, yy)
  }
  var xx = asarray(x.value)
  if (yy.length == 1) {
    yy = list(repeat(yy[0], xx.length))
  } else if (xx.length == 1) {
    xx = list(repeat(xx[0], yy.length))
  }
  if (xx.length != yy.length) {
    return "Error: mismatched lengths (" + y + " and " + x + ")"
  }
  return map(fun, yy, xx).join(' ')
}

// binary numerical operation
function bin_num_op(fun, op) {
  append_output() 
  var x = pop_stack()
  var y = pop_stack()
  push_stack_with_preds([y, x], { value: bin_apply_fun(fun, y, x),
                        expr: get_expr(y) + " " + op + " " + get_expr(x)})
  return false
}

function push_reducer() {
  append_output()
  push_stack({value: '(now use "+" to sum an array or "*" for its product)', 
    expr: 'reduce', atomic: 1})
}

// unary numerical operation
function un_num_op(fun, op) {
  append_output() 
  var v = pop_stack()
  push_stack_with_preds([v], {value: map(fun, asarray(v.value)).join(' '), 
    expr: op + get_expr(v)})
  return false
}

function iota() {
  append_output() 
  var v = pop_stack()
  push_stack_with_preds([v], {value: list(range(v.value)).join(' '), 
    expr: 'iota ' + get_expr(v)})
  return false
}

function explode() {
  append_output()
  var v = pop_stack()
  var vv = asarray(v.value)
  if (vv.length > 1) {
    var val = vv.shift()
    push_stack({value: val, expr: val, atomic: 1})
    push_stack_with_preds([v], {value: vv.join(' '), 
      expr: 'tail ' + get_expr(v)})
  } else {
    push_stack(v)
    push_stack({value: "Error: can't explode a scalar", expr: '@' + v.value,
               atomic: 1})
  }
  return false
}

function make_array() {
  append_output()
  var a = pop_stack()
  var b = pop_stack()
  push_stack_with_preds([b, a], {value: [b.value, a.value].join(' '),
      expr: get_expr(b) + ", " + get_expr(a)})
  return false
}

// swap top two items on stack.
function swap_stack() {
  var x = pop_stack()
  var y = pop_stack()
  push_stack(x)
  push_stack(y)
}

var binary_operators = {
  '+': operator.add,
  '*': operator.mul,
  '/': operator.div,
  '-': operator.sub,
  '^': Math.pow,
}

var unary_operators = {
  e: [Math.exp, 'exp '],
  l: [Math.log, 'ln '],
  a: [Math.atan, 'arctan '],
  s: [Math.sin, 'sin '],
  c: [Math.cos, 'cos '],
  r: [function(x){return 1/x}, '1/'],
  _: [function(x){return -x}, '-'],
}
  
function scroll_input_into_view() {
  var inp = $('the_input')
  window.scrollTo(0, elementPosition(inp).y
                  + $(inp).scrollHeight/2
                  - document.body.clientHeight/2)
}

///////// Graph resizing
// Major problems:
// - hardcoded icon size

graph_to_resize = null

function resize_graph(icon, prev_pos) {
  log('when starting resize, pos is', prev_pos)
  return function handle_resize_mousemove(ev) {
    var current_pos = ev.mouse().page
    log('current_pos is', current_pos)
    var dx = current_pos.x - prev_pos.x
    var dy = current_pos.y - prev_pos.y
    prev_pos = current_pos
    graph_to_resize.width += dx
    graph_to_resize.height += dy
    icon.style.left = parseInt(icon.style.left) + dx
    icon.style.top = parseInt(icon.style.top) + dy
    log('icon.style.left is', icon.style.left, 'dx was', dx)
    redraw_graph(graph_to_resize)
  }
}

resizing = 0
function stop_resizing(move_handler) {
  return function stop_resizing_handler(ev) {
    log('stopping resizing')
    disconnect(move_handler)
    resizing = 0
    $('resize_icon').style.display = 'none'
  }
}

function start_resizing(icon, graph) {
  return function handle_resize_mousedown(ev) {
    resizing = 1
    var move_handler = connect(document, 'onmousemove', 
                               resize_graph(icon, ev.mouse().page))
    var stop = stop_resizing(move_handler)
    connect(document, 'onmouseup', stop)
    ev.stop()  // to prevent normal image-drag-and-drop behavior
  }
}

// generated by
// >>> base64.encodestring(file('resize-icon.png').read()).replace('\n', '')
data_url = ('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA' +
            'BAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAABl0R' +
            'Vh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHaS' +
            'URBVDiNnZLdUhJxGMZ/u2ZAjeVHDA1lA4aKptlJzdQtNF2BR' +
            '3XcjGcddNKtdA9tN2AfltFICasksgSuIIbs0u5SsMu/A8cdM' +
            'Zkmn8P3eZ/n/ZSEEJwFjqZI6Uw+J59RfH67uLvctJzJc/8pj' +
            'ABPdnb3lyp79cjevsE/DRxNCQCPgEXg4cZWaXBdLXB7dgK9W' +
            'u9v4GiKDCwBL4DLruvxKZ2jpNeIjV+l7boApxs4mjIFvATuA' +
            '1h2i7erGcymjSxJzCVjrGXypxs4mhIEXgMJgGrtgJWUSrtzW' +
            'HEiFuXihSBm0+7bwfMjcS5f5kt2m6NDD8gys1M36Lgult0Co' +
            'OeMjqZcAZ4BlPUa6WNigPmZOKFgAMO0/NjJP1gEAkbT4uPaZ' +
            'g9xPRpmOjEOQMPob/C43enwbjWL53X94OjwEPfuTAPgdbtsa' +
            'brP+TtwNOWuEGLhQ2rDnw9gMn6NhbmbDMiHtbYKeg9/fIlP1' +
            'zc1KrUDAMZGLjE/EycSHvET6o0maq7Y07IkhMDRlHBpp6avf' +
            'FYHR4eHuJWMEY2MASCEoGFaqN++o1d+cBKSXXglF8vVN1qp+' +
            'iARixIKBTB/2phNm4ZhYZgWruf9JfRH+KoWsrbzK+m0fvM+p' +
            'fZN7Ic/LYrgTabsDIoAAAAASUVORK5CYII=')

function mouseover_display_resize_icon(ev) {
  var graph = ev.target()
  var img = $('resize_icon')
  if (!img) {
    img = IMG({id: 'resize_icon', src: data_url})
    document.body.appendChild(img)
    connect(img, 'onmouseout', mouseout_hide_resize_icon)
    connect(img, 'onmousedown', start_resizing(img, graph))
  }
  img.style.display = ''
  img.style.position = 'absolute'
  var pos = elementPosition(graph)
  // when we first get here, for some reason img.height and img.width
  // are 24, so I just hardcoded 16 here
  img.style.top = pos.y + graph.height - 16
  img.style.left = pos.x + graph.width - 16
  graph_to_resize = graph
}

function mouseout_hide_resize_icon(ev) {
  if (ev.relatedTarget() != $('resize_icon')) {
    $('resize_icon').style.display = 'none'
  }
}

///////// Keyboard user interface

// called when a printable key is pressed and released in the input field
function handle_keypress(evt) {
  log('press', evt.key().toSource())
  var key = evt.key()
  var code = key.code
  var chr = key.string
  //debug_output(key)
  if (chr in binary_operators) {
    bin_num_op(binary_operators[chr], chr)
  } else if (chr in unary_operators) {
    var op = unary_operators[chr]
    un_num_op(op[0], op[1])
  } else if (chr == 'i') { // 'i' for 'iota'
    iota()
  } else if (chr == ',') { // ',' to append arrays
    make_array()
  } else if (chr == '@') { // '@' to expand one
    explode()
  } else if (chr == '.' || chr == ' ' || chr >= '0' && chr <= '9') {
    return
  } else if (chr == '?') {
    push_reducer()
  } else if (code == 0) { // some special key
    //debug_output(evt._event)
    // We can't reliably prevent Enter or Tab from propagating normally
    // in the places where their UI actions are handled, so we do it here:
    var prim = evt._event
    var mod = evt.modifier()
    if (!mod.any && (prim.keyCode == 13 || prim.keyCode == 9)) { }
    else if (prim.keyCode == prim.DOM_VK_DOWN) {
      // Also, if we try to handle 'up' and 'down' in keyup or
      // keydown, we lose auto-repeat.
      if (mod.shift && !mod.ctrl && !mod.alt && !mod.meta) drag_down()
      else if (!mod.any) go_down()
      else return
    } else if (prim.keyCode == prim.DOM_VK_UP) {
      if (mod.shift && !mod.ctrl && !mod.alt && !mod.meta) drag_up()
      else if (!mod.any) go_up()
      else return
    } else return
  } else if (chr == 'z') {  // "undo"
    split_value()
  }
  // if we didn't return somewhere along the way, stop the propagation
  // of the event:
  evt.stop()
  scroll_input_into_view()
}

function handle_keyup(evt) {
  //debug_output(merge({'up': 1}, key))
  log('up', evt.key().toSource())
  if (evt.key().string == 'KEY_ENTER') {
    // XXX: I don't remember why I thought it was better to handle this in
    // keyup instead of keydown.
    if (empty_field()) {
      var val = pop_stack()
      push_stack(val)
      push_stack(val)
    }
    append_output()
    // This doesn't work here if the enters are generated by key
    // repeat, so we do it in handle_keypress instead:
    // evt.stop()
  }
  scroll_input_into_view()
}

function handle_keydown(evt) {
  var string = evt.key().string
  log('down', evt.key().toSource())
  // We must handle KEY_TAB in keydown instead of keyup because, if we
  // don't cancel it, we lose focus and never get the keyup.
  if (string == 'KEY_TAB') {
    append_output()
    swap_stack()
    // This has the same problem as stopping Enter in handle_keyup,
    // and the same workaround:
    // evt.stop()
  } else if (string == 'KEY_BACKSPACE') {
    // If we handle backspace in keyup, then the field may already
    // have been emptied, which means the backspace would have both
    // deleted a digit and the top of the stack.
    // In this case, not handling auto-repeated backspace is kind of a
    // bonus:
    if (!handle_backspace()) evt.stop()
  }
  scroll_input_into_view()
}

function startup() {
  $('the_input').focus()
  connect('the_input', 'onkeypress', handle_keypress)
  connect('the_input', 'onkeyup', handle_keyup)
  connect('the_input', 'onkeydown', handle_keydown)
  $('the_input').name = new Date().getTime()  // break autocomplete :)
}

