/***************************************************************************
 *   Copyright (C) 2008, Paul Lutus                                        *
 *                                                                         *
 *   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.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

window.onload = init;
window.onunload = write_cookie;

var chart_width;
var chart_height;

var default_font_size = "80%";

var canvas;
var canvas_ctx;
var text_div;
var chart_wrapper;
var control_panel;
var left_margin = 8;
var right_margin = 8;
var top_margin = 8;
var bottom_margin = 8;
var graph_dims;
var chart_title;
var x_axis_data = null;
var y_axis_data = null;
var linewidth;
var plot_steps;
var control_a_var;
var control_b_var;
var control_c_var;
var plot_function_2d;
var grid_color = "rgb(128,192,128)";
var plot_color = "rgb(64,0,255)";
var explore_color = "rgb(128,0,0)";
var error_flag;
var mousedown = false;

function complex_class(x,y) {
  this.x = x;
  this.y = y;
}

function graph_dim_class(xl,xh,yl,yh) {
  this.xl = xl;
  this.xh = xh;
  this.yl = yl;
  this.yh = yh;
  this.str = function() {
    return this.xl + "," + this.xh + "," + this.yl + "," + this.yh;
  }
}

function axis_data_class(min,max,steps,label,nums) {
  this.min = min;
  this.minlimit = min * 8;
  this.max = max;
  this.maxlimit = max * 8;
  this.steps = steps;
  this.increm = (max-min)/steps;
  this.label = label;
  this.nums = nums;
  this.str = function() {
    return this.min + "," + this.max + "," + this.steps + "," + this.increm + "," + this.label + "," + this.nums;
  }
}

function min(a,b) {
  return (a < b)?a:b;
}

function max(a,b) {
  return (a > b)?a:b;
}

// All that typing!

function id(s) {
  return document.getElementById(s);
}

// avoid extraneous arguments when tagging the cookie

function bare_url(url) {
  return url.replace(/^(.*?)(#|\?).*$/,"$1");
}

function read_cookie()
{
  var url = bare_url(window.location.href);
  var result = null;
  if(document.cookie.match(url)) {
    result = document.cookie.replace(new RegExp(".*" + url + "=(.*?)(;|$).*"),"$1");
    if (result && result.length > 0) {
      decode_piped_list(unescape(result));
    }
  }
  else { // no cookie, load default example
    decode_example("square");
  }
}

function write_cookie() {
  var cookie_val = encode_piped_list();
  var url = bare_url(window.location.href);
  //alert("write: " + url + "\n\n" + cookie_val)
  // thirty-day timeout on this cookie
  var exp = new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000);
  document.cookie = url + "=" + escape(cookie_val) + "; expires=" + exp.toGMTString();
}

function strip_whitespace(s) {
  return s.replace(/^(\s*)(.*?)(\s*)$/,"$2");
}

// piped list to application controls

function decode_piped_list(list) {
  var array = list.split("#");
  //alert("decode : " + array + "\n\n" + array[0])
  for(i = 0;i < array.length;i += 2) {
    var ident = array[i];
    var val = array[i+1];
    // strip white space from beginning and end
    val = strip_whitespace(val);
    var obj = id(ident);
    if(obj) {
      switch(obj.type) {
        case "checkbox":
          obj.checked = (val == "true");
        break;
        case "text":
          obj.value = val;
        break;
        case "hidden":
          obj.value = val;
        break;
        default:
          //obj.innerHTML = val;
        obj.value = val;
      }
      if(ident == "comment_div" && val.length > 0) {
        obj.parentNode.className = "visible_class"; // not "hidden_class"
      }
    }
  }
}

// application controls to piped list

function encode_piped_item(data,ob) {
  var ident = ob.id;
  var val = ob.value;
  // strip white space from beginning and end
  val = strip_whitespace(val);
  if(ident.length > 0 && ! ident.match(/^x_/)) {
    switch(ob.type) {
      case "checkbox":
        data.push(ident);
      data.push(ob.checked);
      break;
      case "text":
        data.push(ident);
      data.push(val);
      break;
      default:
        data.push(ident);
      data.push(val);
      break;
    }
  }
  return data;
}

function encode_piped_list() {
  var data = new Array();
  array = document.getElementsByTagName("input");
  for(i = 0;i < array.length;i++) {
    data = encode_piped_item(data,array[i]);
  }
  data = encode_piped_item(data,id("comment_div"));
  return data.join("#");
}

function insert_control_list() {
  var data = encode_piped_list();
  // don't keep the chart size,
  // user may have chosen a different size
  data = data.replace(/chart(Width|Height).*?\d+#/g,"");
  // escape quotes
  data = data.replace(/"/g,"\\\"");
  // escape linefeeds
  data = data.replace(/\n/g,"\\n");
  var target = id("edit_div");
  target.innerHTML = "\"" + data + "\";";
  open_edit_div();
}

function retrieve_control_value(s) {
  var obj = id(s);
  return ((obj.type == "checkbox")?obj.checked:obj.value);
}

function retrieve_control_number(s) {
  sv = retrieve_control_value(s);
  v = 0;
  a = control_a_var;
  b = control_b_var;
  c = control_c_var;
  try {
    with (Math) {
      v = eval(sv);
    }
  }
  catch(err) {
    //alert(err);
    error_flag = true;
  }
  return v;
}

function retrieve_axis_data(s) {
  var q = new axis_data_class(
    retrieve_control_number(s + "Min2DLineEdit"),
    retrieve_control_number(s + "Max2DLineEdit"),
    retrieve_control_number(s + "GridStepsLineEdit"),
    retrieve_control_value(s + "LabelLineEdit"),
    retrieve_control_value(s + "IndexCheckBox")
  );
  return q;
}

function update_axis_data() {
  x_axis_data = retrieve_axis_data("x");
  y_axis_data = retrieve_axis_data("y");
  linewidth = retrieve_control_number("lineThicknessLineEdit");
  control_a_var = retrieve_control_number("controlALineEdit");
  control_b_var = retrieve_control_number("controlBLineEdit");
  control_c_var = retrieve_control_number("controlCLineEdit");
  plot_steps = retrieve_control_number("plotStepsLineEdit");
  plot_function_2d = retrieve_control_value("equation2DLineEdit");
  chart_title = retrieve_control_value("chartTitleLineEdit");
  chart_title = chart_title.replace(/\?/,plot_function_2d);
}

function set_wh(obj,w,h) {
  obj.style.width = w + "px";
  obj.style.height = h + "px";
}

function resize_graph() {
  chart_width = retrieve_control_number("chartWidthLineEdit");
  chart_height = retrieve_control_number("chartHeightLineEdit");
  set_wh(chart_wrapper,chart_width,chart_height);
  set_wh(canvas,chart_width,chart_height);
  canvas.width = chart_width;
  canvas.height = chart_height;
  set_wh(text_div,chart_width,chart_height);
}

function resize_plot() {
  resize_graph();
  plot_graph();
}

function set_title_strings() {
  var ob,class_name,ident,i;
  array = document.getElementsByTagName("input");
  for(i = 0;i < array.length;i++) {
    ob = array[i];
    class_name = ob.className;
    ident = ob.id;
    if ((class_name.match(/input_field/i))
      && ! (ident.match(/equation/i))
      // && ! (ident.match(/chart(Width|Height)/i))
      ) {
      ob.title = "Change value with mouse wheel.";
    }
  }
}

function hide_show_divs(array,show) {
  for(var i = 0;i < array.length;i++) {
    div = id(array[i]);
    div.className = (show)?"visible_class":"hidden_class";
  }
}

function open_edit_div() {
  var target = id("edit_div");
  target.className = "edit_div"; // from "hidden_class"
}

function open_comment_list() {
  hide_show_divs(["comment_wrapper"],true);
}

function close_comment_list() {
  hide_show_divs(["comment_wrapper"],false)
}

function decode_example(s) {
  var data = "";
  eval("data = example_" + s);
  decode_piped_list(data);
}

function launch_example(s) {
  decode_example(s);
  plot_graph();
  window.location.href = "#x_scrolldown";
}

function reset_normal() {
  window.location.href = bare_url(window.location.href);
}

function decode_queries() {
  result = [];
  var path = window.location.href;
  var args = path.replace(/^(.*?)(\?|$)(.*)/,"$3");
  args = strip_whitespace(args);
  if(args.length > 0) {
    var array = args.split("&");
    for(var i = 0;i < array.length;i++) {
      var pair = array[i].split("=");
      if(pair.length == 2) {
        result[strip_whitespace(pair[0])] = strip_whitespace(pair[1]);
      }
    }
  }
  return result;
}

function init() {
  read_cookie();
  // set up mouse wheel detection
  if (window.addEventListener) {
    window.addEventListener('DOMMouseScroll', wheel_handler, false);
  }
  
  document.onmousewheel = wheel_handler;
  //window.onmousewheel = wheel_handler;
  document.onkeypress = key_handler;
  //window.onkeypress = key_handler;
  
  document.onmousedown = mouse_down;
  document.onmouseup = mouse_up;
  //window.onmousedown = mouse_down;
  //window.onmouseup = mouse_up;
  
  canvas = id("graphicPane1");
  canvas_ctx = canvas.getContext("2d");
  text_div = id("textPane1");
  chart_wrapper = id("chart_wrapper");
  control_panel = id("control_panel");
  resize_graph();
  set_title_strings();
  plot_graph();
  solve_example();
}

function perform(ident) {
  if(ident.match(/chart(Width|Height)/i)) {
    resize_graph();
  }
  if(ident.match(/example/)) {
    solve_example();
  }
  else {
    plot_graph();
  }
}

function key_handler(event) {
  var char = null;
  if(!event) {
    event = window.event;
  }
  if(event.keyCode && event.keyCode == 13) {
    if(event.target) target = event.target;
    else if (event.srcElement) target = event.srcElement;
    ident = target.id;
    perform(ident);
  }
}

function window_height() {
  return (window.innerHeight)?window.innerHeight:document.body.offsetHeight;
}

function window_width() {
  return (window.innerWidth)?window.innerWidth:document.body.offsetwidth;
}

function mouse_handler(event) {
  if(mousedown) {
    if(!event) {
      event = window.event;
    }
    if(event.target) {
      target = event.target;
    }
    else if (event.srcElement) {
      target = event.srcElement;
    }
    if (target.nodeType == 3) { // defeat Safari bug
      target = target.parentNode;
    }
    // When the canvas emulator is in use,
    // the lines on the graph are not normal HTML
    // objects. They belong to G_vml_.
    // So:
    if(target.id == "textPane1" ||
      target.id == "graphicPane1" ||
      target.id == "text_span" ||
      target.scopeName == "g_vml_") {
      // draw a crosshair and values
      var xpos = (event.layerX)?event.layerX:event.x;
      draw_mouse_query(xpos);
      return false;
    }
  }
  return true;
}

function mouse_down(event) {
  mousedown = true;
  return mouse_handler(event);
}

function mouse_up() {
  mousedown = false;
  plot_graph();
  return false;
}

function wheel_handler(event) {
  var delta = 0;
  var sv,tv,nv;
  if(!event) {
    event = window.event;
  }
  if (event.wheelDelta) {
    delta = event.wheelDelta/120;
    if (window.opera)
      delta = -delta;
    } else if (event.detail) {
    delta = -event.detail/3;
  }
  if (event.target) {
    target = event.target;
  }
  else if (event.srcElement) {
    target = event.srcElement;
  }
  if (target.nodeType == 3) // defeat Safari bug
    target = target.parentNode;
  var name = target.id;
  var width_height = (name.match(/chart(Width|Height)/i));
  if ((name && name.match(/LineEdit/))
    && ! (name.match(/equation/i))
    // && ! width_height
    ) {
    // separate numeric from algebraic
    tv = target.value;
    nv = tv.replace(/([+-\.\d]+).*/,"$1")
    sv = tv.replace(/[+-\.\d]+(.*)/,"$1")
    // force numeric context
    nv = 1 * nv;
    // proportionally scale the result
    var delta2 = parseInt(Math.abs(nv / 20));
    delta2 = delta2 * delta + delta;
    if(!isNaN(nv)) {
      // recombine numeric and algebraic
      target.value = nv + delta2 + sv;
      // these mustn't fall below zero
      if(name.match(/thickness/i) || (name.match(/steps/i))) {
        target.value = (target.value < 1)?1:target.value;
      }
    }
    if(event.stopPropagation)
      event.stopPropagation();
    if(event.preventDefault)
      event.preventDefault();
    event.cancelBubble = true;
    event.cancel = true;
    event.returnValue = false;
    perform(name);
    return false;
  }
  else {
    return true;
  }
}

function solve_example() {
  var dest = id("x_examplem_result");
  var a = id("x_examplem_aLineEdit").value;
  var op = id("x_examplem_opLineEdit").value;
  var b = id("x_examplem_bLineEdit").value;
  var y = null;
  try {
    with (Math) {
      var arg = a + " " + op + " " + b;
      y = eval(arg);
      if(isNaN(y)) throw("Entry error");
    }
  }
  catch (error) {
    y = error;
  }
  if(y != null) {
    dest.innerHTML = y;
  }
}

function draw_line(x1,y1,x2,y2) {
  try {
    canvas_ctx.moveTo(x1,y1);
    canvas_ctx.lineTo(x2,y2);
  }
  catch(err) {
    error_flag = true;
  }
}

// interpolate x from xa,xb -> ya,yb

function ntrp(xa,xb,ya,yb,x) {
  var q;
  if((q = xb-xa) == 0) return 0;
  return ((x-xa)/q * (yb-ya)) + ya;
}

function plot_grid() {
  canvas_ctx.beginPath();
  var px,py;
  var ox = null;
  var oy = null;
  canvas_ctx.strokeStyle = grid_color;
  for(var x = x_axis_data.min;x <= x_axis_data.max;x += x_axis_data.increm) {
    px = ntrp(x_axis_data.min,x_axis_data.max,graph_dims.xl,graph_dims.xh,x);
    // deal with a perfectly insane canvas bug
    px = parseInt(px) + 0.5;
    draw_line(px,graph_dims.yl,px,graph_dims.yh);
  }
  for(var y = y_axis_data.min;y <= y_axis_data.max;y += y_axis_data.increm) {
    py = ntrp(y_axis_data.min,y_axis_data.max,graph_dims.yl,graph_dims.yh,y);
    // deal with a perfectly insane canvas bug (again)
    py = parseInt(py) + 0.5;
    draw_line(graph_dims.xl,py,graph_dims.xh,py);
  }
  canvas_ctx.stroke();
  canvas_ctx.closePath();
}

function eval_function(x) {
  y = 0;
  var a = control_a_var;
  var b = control_b_var;
  var c = control_c_var;
  try {
    with (Math) {
      y = eval(plot_function_2d);
      if(isNaN(y)) throw("Function error");
      // limit range of y results
      y = min(y,y_axis_data.maxlimit);
      y = max(y,y_axis_data.minlimit);
    }
  }
  catch(error) {
    show_graph_error(error);
    error_flag = true;
  }
  return y;
}

function plot_function() {
  canvas_ctx.strokeStyle = plot_color;
  canvas_ctx.beginPath();
  var x,px;
  var y,py;
  var first = true;
  var incr = (x_axis_data.max-x_axis_data.min)/plot_steps;
  for(x = x_axis_data.min;!error_flag && x <= x_axis_data.max;x += incr) {
    px = ntrp(x_axis_data.min,x_axis_data.max,graph_dims.xl,graph_dims.xh,x);
    y = eval_function(x);
    py = ntrp(y_axis_data.min,y_axis_data.max,graph_dims.yh,graph_dims.yl,y);
    try {
      if(first) {
        canvas_ctx.moveTo(px,py);
      }
      else {
        canvas_ctx.lineTo(px,py);
      }
    }
    catch(err) {
      error_flag = true;
    }
    first = false;
  }
  canvas_ctx.stroke();
  canvas_ctx.closePath();
}

function clearChildren(obj)
{
  try {
    if(obj.hasChildNodes() && obj.childNodes) {
      while(obj.firstChild) {
        obj.removeChild(obj.firstChild);
      }
    }
  }
  catch(e) {
  }
}

function create_text_span(s,ps) {
  var tsp = document.createElement('span');
  tsp.id = "text_span";
  var ts = tsp.style;
  ts.fontSize = ps;
  ts.whiteSpace = "nowrap";
  ts.fontFamily = "monospace";
  var tn = document.createTextNode(s);
  tsp.appendChild(tn);
  return tsp;
}

function create_index_span(parent,str,x,y,max,align,ps) {
  var tsp = create_text_span(str,ps);
  parent.appendChild(tsp);
  var ts = tsp.style;
  ts.position = "absolute";
  ts.textAlign = align;
  ts.width = (max*8) + "px"
  ts.left = x + "px";
  ts.top = y + "px";
}

function gen_y_index(obj) {
  var text_delta = 8;
  var maxw = y_axis_data.label.length;
  var w;
  var array = [];
  for(var y = y_axis_data.min;y <= y_axis_data.max;y += y_axis_data.increm) {
    py = ntrp(y_axis_data.min,y_axis_data.max,graph_dims.yh,graph_dims.yl,y);
    sy = y.toFixed(2);
    array.push(sy + "\t" + py);
    w = ("" + sy).length;
    maxw = (w > maxw)?w:maxw;
  }
  create_index_span(obj,y_axis_data.label,4,graph_dims.yl-text_delta-16,maxw,"right",default_font_size);
  var pair;
  for(i = 0;i < array.length;i++) {
    pair = array[i].split("\t");
    create_index_span(obj,pair[0],4,pair[1]-text_delta,maxw,"right",default_font_size);
  }
  graph_dims.xl += (maxw * 8);
}

function gen_x_index(obj) {
  var deltax = 0;
  var maxw = x_axis_data.label.length;
  var text_delta = 6;
  var w;
  var array = [];
  for(var x = x_axis_data.min;x <= x_axis_data.max;x += x_axis_data.increm) {
    sx = x.toFixed(2);
    array.push(sx + "\t" + x);
    w = ("" + sx).length;
    maxw = (w > maxw)?w:maxw;
  }
  var len = x_axis_data.label.length;
  var maxx = (maxw > len)?len:maxw;
  graph_dims.xh -= maxx * 8;
  for(i = 0;i < array.length;i++) {
    pair = array[i].split("\t");
    px = ntrp(x_axis_data.min,x_axis_data.max,graph_dims.xl,graph_dims.xh,pair[1]);
    create_index_span(obj,pair[0],px-maxw*4,graph_dims.yh+text_delta,maxw,"center",default_font_size);
  }
  create_index_span(obj,x_axis_data.label,graph_dims.xh+24,graph_dims.yh+text_delta,maxx,"right",default_font_size);
}

function graph_indices() {
  if(text_div != null) {
    if(y_axis_data.nums) {
      gen_y_index(text_div);
    } // x axis index
    if(chart_title.length > 0) {
      var title = chart_title.replace(/\?/,plot_function_2d);
      var center = (graph_dims.xl + graph_dims.xh-(title.length*10))/2;
      create_index_span(text_div,title,center,4,title.length,"center","100%");
    }
    if(x_axis_data.nums) {
      gen_x_index(text_div);
    } // y axis index
  } // text_div != null
} // graph_title()

function set_dimensions() {
  var bottom = canvas.height - bottom_margin - ((x_axis_data.nums)?16:0);
  var left = left_margin + ((x_axis_data.nums)?16:0);
  var right = canvas.width - right_margin - ((x_axis_data.nums)?16:0);
  var top = top_margin + ((chart_title.length > 0 || y_axis_data.nums)?20:0);
  graph_dims = new graph_dim_class(left,right,top,bottom);
}

function plot_graph() {
  clearChildren(text_div);
  canvas_ctx.clearRect(0, 0, canvas.width, canvas.height);
  update_axis_data();
  if((x_axis_data.max > x_axis_data.min) &&
    (y_axis_data.max > y_axis_data.min) &&
    x_axis_data.increm > 0 &&
    y_axis_data.increm > 0 &&
    plot_steps > 0) {
    error_flag = false;
    set_dimensions();
    graph_indices();
    if(!error_flag) {
      canvas_ctx.globalAlpha = 1;
      canvas_ctx.lineWidth = "" + linewidth;
      canvas_ctx.lineCap = "round";
      plot_grid();
      plot_function();
    }
  }
  else { // range error
    show_graph_error("numerical range");
  }
  return false;
}

function show_graph_error(s) {
  clearChildren(text_div);
  canvas_ctx.clearRect(0, 0, canvas.width, canvas.height);
  var xm = canvas.width/2;
  var ym = canvas.height/2;
  var str = "Error: " + s;
  create_index_span(text_div,str,xm-(str.length*4),ym,str.length,"center","120%");
}

function draw_mouse_query(x) {
  var fx = ntrp(graph_dims.xl,graph_dims.xh,x_axis_data.min,x_axis_data.max,x);
  y = eval_function(fx);
  px = ntrp(x_axis_data.min,x_axis_data.max,graph_dims.xl,graph_dims.xh,fx);
  px = parseInt(px) + 0.5;
  py = ntrp(y_axis_data.min,y_axis_data.max,graph_dims.yh,graph_dims.yl,y);
  py = parseInt(py) + 0.5;
  canvas_ctx.strokeStyle = explore_color;
  canvas_ctx.beginPath();
  draw_line(graph_dims.xl,py,graph_dims.xh,py);
  draw_line(px,graph_dims.yl,px,graph_dims.yh);
  canvas_ctx.stroke();
  canvas_ctx.closePath();
  str = "x =" + fx.toFixed(6) + ", y = " + y.toFixed(6);
  // decide where to put the number tag
  if(fx > (x_axis_data.min+x_axis_data.max)/2) {
    px -= (str.length * 8) + 4;
  }
  else {
    px += 8;
  }
  if(y < (y_axis_data.min+y_axis_data.max)/2) {
    py -= 20;
  }
  else {
    py += 4;
  }
  create_index_span(text_div,str,px,py,str.length,"left",default_font_size);
}