/*
 * A dependency graph for propagating changes, with a few extra features:
 * - an extra list of needed nodes, determining whether a node should be
 *   loaded (okValue for all needed) or cleared (otherwise) on update
 * - on change, notify downstream nodes, pending update
 * - pass upstream nodes (with okValue) to load
 * - excluded nodes, never called, not okValue (changes can propagate through)
 * - A node has five methods:
 *   - load: update self, needed met and okToLoad.  First arg the set of
 *     upstream nodes (with okValue); second a continuation so it may be
 *     asynchronous.
 *   - clear: update self, needed not met or okToLoad false.
 *   - okToLoad (optional): should you be loaded (if not, cleared)?  First
 *     arg the set of upstream nodes (with okValue).
 *   - invalidate: invalidate self (load or clear will be called soon)
 *   - okValue: do you have an ok value? (for needed and upstream set)
 * - the graph should be acyclic
 * - branches may join; a change only propagates to a node after propagating
 *   along all paths to it.
 * - NB the values of next are lists giving node orders; values of
 *   needed are sets
 * - NB we don't remember whether a node is loaded or cleared, and we don't
 *   have a notion of value for a node; you have to do that
 * TODO be forgiving of all nodes not appearing in next, needed
 */
function makeGraph(nodes, next, needed) {
  var downstream = downstreamCount(next);
  var upstream = transitiveClosure(reverseGraph(next));
  var excluded = set();
  var g = {
    setExcluded: function (newExcluded) {
      var toUpdate = union(excluded, newExcluded);
      excluded = newExcluded;
      var toVisit = {};
      for (var n in toUpdate) {
        multisetUnionTo(toVisit, downstream[n]);
      }
      g.invalidate(toVisit);
      for (n in toUpdate) {
        g._update(n, toVisit);
      }
    },
    changed: function (name) {
      var toVisit = clone(downstream[name]);
      multisetIncr(toVisit, name, -1);
      g.invalidate(toVisit);
      g.updateNext(name, toVisit);
    },
    update: function (name) {
      var toVisit = clone(downstream[name]);
      g.invalidate(toVisit);
      g._update(name, toVisit);
    },
    invalidate: function (toVisit) {
      for (var ds in toVisit) {
        if (ds in excluded) continue;
        nodes[ds].invalidate();
      }
    },
    _update: function (name, toVisit) {
      // even if skipping, continue downstream to get the counts right
      if (--toVisit[name] > 0 || name in excluded) {
        g.updateNext(name, toVisit);
        return;
      }
      var reqsMet = true;
      for (var req in needed[name]) {
        if (req in excluded || !nodes[req].okValue()) {
          reqsMet = false;
          break;
        }
      }
      var okUpstream = {};
      for (n in upstream[name]) {
        if (!(n in excluded) && nodes[n].okValue()) {
          okUpstream[n] = undefined;
        }
      }
      if (reqsMet &&
          (!nodes[name].okToLoad || nodes[name].okToLoad(okUpstream))) {
        nodes[name].load(okUpstream, function () {
          g.updateNext(name, toVisit);
        });
      } else {
        nodes[name].clear();
        g.updateNext(name, toVisit);
      }
    },
    updateNext: function (name, toVisit) {
      for (var i=0; i<next[name].length; i++) {
        g._update(next[name][i], toVisit);
      }
    }
  };
  return g;
}

/* A node representing an attribute with a select control. */
function makeAttrNode(url, name, selectId, params) {
  var http = new MyHttp('GET', url + name + '.json?' + (params ? params : ''));
  return {
    load: function (upstream, k) {
      http.get(formArgs(elem(selectId).form.id, upstream), function (b) {
        fillSelect(selectId, readJSON(b)[name]);
        if (elem(selectId+'_l')) {
          ungray(selectId+'_l');
        }
        k();
      }, function (b) { });
    },
    clear: function () {
      clearSelect(selectId);
      if (elem(selectId+'_l')) {
        gray(selectId+'_l');
      }
    },
    invalidate: function () {
      http.abort();
      disable(selectId);
    },
    okValue: function () { return selectValue(selectId); }
  }
}
function fillSelect(id, opts) {
  var vOld = selectValue(id);
  clearSelect(id);
  for (var i=0; i<opts.length; i++) {
    var o = addSelectOption(id, opts[i]['id'], opts[i]['name']);
    if (opts[i]['id'] == vOld) {
      o.selected = true;
    }
  }
  enable(id);
}
function addSelectOption(id, value, text) {
  var o = document.createElement('option');
  o.value = value;
  o.text = text;
  // select.add broken in IE (bug 388710)
  var select = elem(id);
  select.options[select.length] = o;
  return o;
}
function clearSelect(id) {
  var select = elem(id);
  for (var i=select.options.length-1; i>0; i--) {
    select.remove(i);
  }
}
function setSelect(id, val) {
  var select = elem(id);
  var found = false;
  for (var i=select.options.length-1; i>0; i--) {
    select[i].selected = select[i].value == val;
    found = found || select[i].value == val;
  }
  if (!found) {
    select[0].selected = true;
  }
}

/* AJAX helpers */
function MyHttp(method, url) {
  this.method = method;
  this.url = url;
  this.http = getHTTPObject();
}
MyHttp.prototype.get = function (args, k, e) {
  this.abort();
  var http = this.http;
  http.onreadystatechange = function () {
    if (http.readyState == 4) {
      if (http.status >= 200 && http.status < 300) {
        k(http.responseText);
      } else {
        e(http.responseText);
      }
    }
  };
  var url = this.url;
  if (this.method == 'GET') {
    url += argsToQS(args);
  }
  http.open(this.method, url);
  http.setRequestHeader('X-TDA-App-Key', '7ACEE90C');
  if (this.method == 'GET') {
    http.send(null);
  } else if (this.method == 'POST') {
    http.setRequestHeader("Content-type",
                          "application/x-www-form-urlencoded;charset=UTF-8");
    http.send(argsToQS(args));
  } else {
    throw("unknown method: " + this.method);
  }
};
MyHttp.prototype.abort = function () {
  // can't detect abort in handler, so just remove it
  this.http.onreadystatechange = function () { }
  this.http.abort();
}
function readJSON(s) { return eval('('+s+')'); }

var handlerCount = 1;
function submitForm(id, k, e) {
  var form = elem(id);
  var targetName = id+'Target'+handlerCount++;
  // workaround for http://webbugtrack.blogspot.com/2007/10/bug-235-createelement-is-broken-in-ie.html
  var d = document.createElement('div');
  d.innerHTML = '<iframe id="'+targetName+'" name="'+targetName+'"></iframe>';
  form.appendChild(d);
  var target = elem(targetName);
  target.style.display = 'none';
  target.src = 'about:blank';
  var onload = function () {
    var doc = target.contentWindow.document;
    if (doc.location.href == 'about:blank') return;
    var xml = doc.XMLDocument ? doc.XMLDocument : doc;
    form.removeChild(d);
    var r = pathNode(xml, ['result']);
    var s = pathNode(xml, ['result', 'status']);
    s && pathString(s, []) == '200' ? k(r) : e(r);
  }
  // http://www.nczonline.net/blog/2009/09/15/iframes-onload-and-documentdomain/
  if (target.attachEvent) {
    target.attachEvent('onload', onload);
  } else {
    target.onload = onload;
  }
  form.target = targetName;
  form.submit();
}

function pathString(n, p) {
  var r = pathNode(n, p);
  return r.firstChild ? r.firstChild.nodeValue : '';
}
function pathNode(n, p) {
  var ns = pathNodes(n, p);
  if (ns.length > 1) throw('expected only one node for path ' + p.join('/'));
  return ns.length == 0 ? null : ns[0];
}
function pathNodes(n, p) {
  if (p.length == 0) return [n];
  var ns = pathChildren(n, p.shift());
  var r = [];
  for (var i=0; i<ns.length; i++) {
    r = r.concat(pathNodes(ns[i], p));
  }
  return r;
}
function pathChildren(n, p) {
  var ns = n.childNodes;
  var r = [];
  for (var i=0; i<ns.length; i++) {
    if (ns[i].tagName == p) {
      r.push(ns[i]);
    }
  }
  return r;
}


/* form helpers */
function formArgs(id, included) {
  var form = elem(id);
  var r = {};
  function addkv(k, v) {
    if (!r[k]) {
      r[k] = new Array();
    }
    r[k].push(v);
  }
  for (var i=0; i<form.elements.length; i++) {
    e = form.elements[i];
    if (e.disabled) continue;
    if (included && !(e.name in included)) continue;
    switch (e.type) {
      case 'text':
      case 'textarea':
      case 'password':
      case 'submit':
      case 'button':
      case 'hidden':
        addkv(e.name, e.value);
        break;
      case 'select-one':
        addkv(e.name, e[e.selectedIndex].value);
        break;
      case 'select-multiple':
        for (var j=0; j<e.options.length; j++) {
          o = e.options[j];
          if (o.selected) {
            addkv(e.name, o.value);
          }
        }
        break;
      case 'checkbox':
      case 'radio':
        if (e.checked) {
          addkv(e.name, e.value);
        }
        break;
      default:
        throw("unknown type "+e.type);
    }
  }
  return r;
}
function selectValue(id) {
  var select = elem(id);
  return select[select.selectedIndex].value;
}
function selectText(id) {
  var select = elem(id);
  return select[select.selectedIndex].text;
}
function argsToQS(args) {
  var a = new Array;
  for (var k in args) {
    for (var i=0; i<args[k].length; i++) {
      a.push(encodeURIComponent(k)+'='+encodeURIComponent(args[k][i]))
    }
  }
  return a.join('&');
}
function emptyInput(id) { return /^\s*$/.test(elem(id).value); }
function monitorInput(id, g, name) {
  var last;
  setInterval(function () {
    if (elem(id).value != last) {
      g.changed(name);
    }
    last = elem(id).value;
  }, 100);
}

/* data structure helpers */

// NB input values are lists, return values are sets
function reverseGraph(g) {
  var r = {};
  for (var k in g) {
    r[k] = {};
  }
  for (var k in g) {
    for (var i=0; i<g[k].length; i++) {
      r[g[k][i]][k] = undefined;
    }
  }
  return r;
}

// input and return values are sets; excludes self
function transitiveClosure(g) {
  var r = {};
  var closureFor = memoize(r, function (k) {
    var r2 = {};
    for (var k2 in g[k]) {
      r2[k2] = undefined;
      unionTo(r2, closureFor(k2));
    }
    return r2;
  });
  for (var k in g) {
    r[k] = closureFor(k);
  }
  return r;
}

// input values are lists, return values are multisets; includes self
function downstreamCount(g) {
  var r = {};
  var downstreamCountFor = memoize(r, function (k) {
    var r2 = {};
    r2[k] = 1;
    for (var i=0; i<g[k].length; i++) {
      multisetUnionTo(r2, downstreamCountFor(g[k][i]));
    }
    return r2;
  });
  for (var k in g) {
    r[k] = downstreamCountFor(k);
  }
  return r;
}
function multisetUnionTo(s1, s2) {
  for (var k in s2) {
    multisetIncr(s1, k, s2[k]);
  }
}
function multisetIncr(s, k, n) {
  if (!(k in s)) {
    s[k] = 0;
  }
  s[k] += n;
  if (s[k] == 0) {
    delete s[k];
  }
}

function memoize(d, f) {
  var mf = function (x) {
    return(d[x] || (d[x] = f(x)));
  }
  return mf;
}

function arrayToSet(a) {
  var r = {};
  for (var i=0; i<a.length; i++) {
    r[a[i]] = undefined;
  }
  return r;
}
function set() { return arrayToSet(arguments); }
function unionTo(s1, s2) {
  for (var k in s2) {
    s1[k] = undefined;
  }
}
function union(s1, s2) {
  var r = clone(s1);
  unionTo(r, s2);
  return r;
}

function clone(o) {
  if (typeof(o) != 'object') return o;
  var r = {};
  for (var k in o) {
    r[k] = clone(o[k]);
  }
  return r;
}

function show(id) { elem(id).style.visibility = 'visible'; }
function hide(id) { elem(id).style.visibility = 'hidden'; }
// http://tobielangel.com/2006/12/31/why-the-css-display-property-sucks/
function display(id) { elem(id).style.display = ''; }
function takeout(id) { elem(id).style.display = 'none'; }
function enable(id)  { elem(id).disabled = false; }
function disable(id) { elem(id).disabled = true; }
function gray(id)   { elem(id).className = 'grayed'; }
function ungray(id) { elem(id).className = ''; }

function popup(url, name) {
  window.open(url, name, 'width=500,height=500,scrollbars=yes,resizable=1');
}

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

function format_number(n) {
  var s = n.toString();
  for (i=s.length-3; i>0; i-=3) {
    s = s.substring(0, i) + ',' + s.substring(i, s.length);
  }
  return s;
}

function format_filename(n) {
  return n.replace(/.*[\/\\]/, '')
}
