// copyright (c) 2006-2008 Franz Inc, Oakland, CA - All rights reserved.
//
// The software, data and information contained herein are proprietary
// to, and comprise valuable trade secrets of, Franz, Inc.  They are
// given in confidence by Franz, Inc. pursuant to a written license
// agreement, and may be stored and used only in accordance with the terms
// of such license.
//
// Restricted Rights Legend
// ------------------------
// Use, duplication, and disclosure of the software, data and information
// contained herein by any agency, department or entity of the U.S.
// Government are subject to restrictions of Restricted Rights for
// Commercial Software developed at private expense as specified in
// DOD FAR Supplement 52.227-7013 (c) (1) (ii), as applicable.

// Visualisation of graphs. Uses the Raphael (lib/raphael.js) library
// for drawing. (Based on SVG or VML.)

function circle(paper, color, scale, x, y) {
  var c = paper.circle(x, y, Math.min(.08 * scale, 8));
  c.attr("stroke-width", 0).attr("fill", color);
  c[0].style.cursor = "pointer";
  return c;
}

function moveCircle(circle, x, y) {
  circle.attr("cx", x).attr("cy", y);
}

function drawNode(node, paper, scale, x, y) {
  if (!node.view) {
    node.view = circle(paper, node.data.url ? "#aaf" : "#afa", scale, x, y);
    node.view[0].node = node;
    node.view[0].part = node.data;
  }
  else {
    moveCircle(node.view, x, y);
  }
};

// Edges are drawn as lines with arrowheads on them, one arrowhead for
// every actual edge that is represented by this multi-edge.
function drawEdge(edge, paper, scale, x1, y1, x2, y2) {
  function triangle(part, flip) {
    var path = paper.path({"stroke-width": 0, fill: "#aaf"}), off = flip ? .06 : -.06;
    path.moveTo(off, 0).lineTo(-off, .05).lineTo(-off, -.05).andClose();
    path[0].part = part;
    path[0].style.cursor = "pointer";
    return path;
  }

  if (!edge.view) {
    var view = edge.view = {};
    view.path = paper.path({stroke: "#aaf", "stroke-width": 2});
    view.path.moveTo(x1, y1).lineTo(x2, y2).andClose();
    view.path.toBack();
    view.arrows = [];
    forEach(edge.backward, function(part) {view.arrows.push(triangle(part, false));});
    forEach(edge.forward, function(part) {view.arrows.push(triangle(part, true));});
  }
  else {
    var path = edge.view.path.path;
    path[0].arg[0] = x1; path[0].arg[1] = y1;
    path[1].arg[0] = x2; path[1].arg[1] = y2;
    edge.view.path.redraw();
  }

  var dir = new Vec(x2 - x1, y2 - y1), pos = new Vec(x1, y1);
  var angle = dir.angle(), sin = Math.sin(angle) * scale, cos = Math.cos(angle) * scale;
  dir.scale(1 / (edge.view.arrows.length + 1));
  forEach(edge.view.arrows, function(arrow) {
    pos.inc(dir);
    arrow.matrix(cos, -sin, sin, cos, pos.x, pos.y);
    arrow.redraw();
  });
};


// Object used to animate a graph model.
function VectorGraphView(model, container, width, height) {
  this.container = container;
  this.width = width || 750; this.height = height || 500;
  this.paper = new Raphael(container, this.width, this.height);
  this.model = model;
}

VectorGraphView.prototype = {
  marginX: 10,
  marginY: 10,

  scale: function(box) {
    box = box || this.model.bounds();
    return Math.min(100, Math.min((this.width - 2 * this.marginX) / (box.maxX - box.minX),
                                  (this.height - 2 * this.marginY)  / (box.maxY - box.minY)));
  },

  // Determine the correct scale and offset (the coordinate system
  // inside the model is independent of screen coordinates), and draw
  // the graph.
  draw: function() {
    var box = this.model.bounds(), self = this, factor = this.scale(box);
    var nodes = this.model.nodes, edges = this.model.edges;

    var x, y;
    function scale(vec) {
      x = (vec.x - box.minX) * factor + 10;
      y = (vec.y - box.minY) * factor + 10;
    }

    for (var i = 0; i < edges.length; i++) {
      var edge = edges[i];
      scale(edge.from.pos);
      var x1 = x, y1 = y;
      scale(edge.to.pos);
      drawEdge(edge, this.paper, factor, x1, y1, x, y);
    }
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      scale(node.pos);
      drawNode(node, this.paper, factor, x, y);
    }
  },

  // Run and redraw the graph until a given number of iterations have
  // been made or until the unrest is small enough. Call c after
  // finishing.
  animate: function(iter, c) {
    if (this.animation) clearTimeout(this.animation);
    var self = this, iter = iter || 100;
    function converge() {
      if (self.model.run() > .002 && iter > 0)
        self.animation = setTimeout(converge, 50);
      else
        c && c();
      iter--;
      self.draw();
    }
    converge();
  }
};

// Drag a node. Use a red circle to show current drag position. When
// no significant move was made (a click), go to the relevant node
// view instead.
function startNodeDrag(view, node, x, y) {
  var curx = x, cury = y, scale = view.scale();
  var px = node.view.attr().cx, py = node.view.attr().cy;
  var dummy = circle(view.paper, "#faa", scale, px, py);
  var move = connect(document, "mousemove", function(e) {
    curx = e.pageX; cury = e.pageY;
    moveCircle(dummy, curx - x + px, cury - y + py);
  });
  var release = connect(document, "mouseup", function() {
    disconnect(move); disconnect(release);
    removeNode(dummy[0]);
    var dx = curx - x, dy = cury - y;
    if (dx * dx + dy * dy <= 4) {
      goTo("node/" + node.data.identity);
    }
    else {
      node.pos.x += dx / scale; node.pos.y += dy / scale;
      node.fixed = true;
      view.animate(60, function(){node.fixed = false;});
    }
  });
}

// Load more triples into a graph model, fetching them from the server
// if needed.
function addGraphTriples(query, model, nodes, pos, upto, c) {
  var triples = query.result.result;
  if (upto > triples.length && query.result.more) {
    fetchMoreQueryResults(query, partial(addGraphTriples, query, model, nodes, pos, upto, c));
    return;
  }
  upto = Math.min(upto, triples.length);
  
  function asNode(partString) {
    if (nodes.contains(partString))
      return nodes.lookup(partString);
    var node = model.node(part(partString));
    nodes.store(partString, node);
    return node;
  }

  for (var i = pos; i < upto; i++) {
    var triple = triples[i];
    model.edge(asNode(triple[0]), asNode(triple[2]), part(triple[1]));
  }
  c && c();
}

// Place a graph animation for a query into the document.
function drawGraph(container, query) {
  connect(container, "mouseover", function(e) {
    if (e.target.part)
      Mold.cast($("graph-context"), template("instruction/part"), e.target.part);
  });
  connect(container, "mousedown", function(e) {
    if (e.target.node) {e.stop(); startNodeDrag(view, e.target.node, e.pageX, e.pageY);}
    else if (e.target.part) goTo("node/" + e.target.part.identity);
  });

  var model = new GraphModel(), nodes = Dictionary.create();
  var view = new VectorGraphView(model, container, 750, 750);
  addGraphTriples(query, model, nodes, 0, 50, function() {
    setTimeout(function() {
      Mold.cast($("graph-addnodes"), template("query/addnodes"), {
        query: query, model: model, view: view, nodes: nodes, pos: Math.min(50, query.result.result.length)
      });
    }, 0);
    view.animate(60);
  });
}

// Called when the user clicks on a control to add triples to the
// model. Update UI, adds triples.
function moreGraphTriples(n, context) {
  $("graph-addnodes").innerHTML = "";
  addGraphTriples(context.query, context.model, context.nodes, context.pos, context.pos + n, function() {
    context.pos = Math.min(context.pos + n, context.query.result.result.length);
    Mold.cast($("graph-addnodes"), template("query/addnodes"), context);
    context.view.animate(60);
  });
}
