/**
* Draw piece trees in <canvas>,
* handle mouse events in piece tree & listing,
* pass swf control commands to methods in player.js
*/

jQuery.fn.piece_tree = function($lis, pieces) {
  return this.each(function() {
		new $.PieceTree(this, $lis, pieces);
	});
};

/**
* PieceTree walks the piece tree passed in and passes high-level drawing commands to the Renderer
*/
jQuery.PieceTree = function(canvas, $lis, pieces) {
  // constants
  var ANIMATION_DURATION = 250;
  
  /**
  * init canvas
  */
  var renderer = jQuery.PieceTree.Renderer(canvas);
  renderer.init_row_heights(pieces.length);
  var piece_tree = null;
  draw_piece_tree();
  
  /**
  * Mouse events
  * use animate() because SWF don't respond to JS with slideDown()/slideUp() for some reason
  */
  $lis.mouseover(function() {
    update_piece_and_redraw($lis.index(this), 'highlight');
  }).mouseout(function() {
    update_piece_and_redraw(null, 'highlight');
  }).bind("show_player", function() {
    $(this).addClass('playing').find('div.player').animate({height: PLAYER_OFFSET_HEIGHT}, ANIMATION_DURATION);
    animate_player_row(li_index_to_piece_index($lis.index(this)), PLAYER_OFFSET_HEIGHT);
  }).bind("hide_player", function() {
    $(this).removeClass('playing').find('div.player').animate({height: '0'}, ANIMATION_DURATION);
    animate_player_row(li_index_to_piece_index($lis.index(this)), '0');
  });
  $lis.find('a.play').click(function() {
    $(this).blur();
    play_piece($(this).parents('li')); // ends up calling above show_player/hide_player events on LI
    return false;
  });
  $(canvas).mousemove(function(e) {
    var piece_index = renderer.mouse_region_piece_index({
      x: e.pageX - this.offsetLeft,
      y: e.pageY - this.offsetTop});
    update_piece_and_redraw(pieces.length-piece_index-1, 'highlight');
  }).mouseout(function() {
    update_piece_and_redraw(null, 'highlight');
  });
  
  /**
  * kick off rendering by feeding root piece into plot_piece, then press GO
  */
  function draw_piece_tree() {
    piece_tree = {dots: [], lines: []};
    plot_piece(pieces[0]);
    renderer.draw_tree(piece_tree);
  }
  
  /**
  * Recursive tree-walking function to prepare drawing commands
  */
  function plot_piece(piece) {
    var new_dot = {
      row: $(pieces).index(piece),
      col: null,
      highlight: piece.highlight};
    
    // assign dot column
    var max_higher_dot_col = $(piece_tree.dots).filter(function() {
      return this.row > new_dot.row;
    }).map(function() {
      return this.col;
    }).max();
    new_dot.col = max_higher_dot_col + 1;
    
    // now recursively call all children
    $(pieces).filter(function() {
      return this.parent_id == piece.id;
    }).each(function() {
      child_dot = plot_piece(this);
      piece_tree.lines.push({from: new_dot, to: child_dot,
        highlight: (piece.highlight || this.highlight)});
    });
    
    piece_tree.dots.push(new_dot);
    return new_dot;
  }
  
  /**
  * Set a piece 'highlight' and redraw canvas (no longer used for 'playing')
  */
  function update_piece_and_redraw(piece_index, attribute_name) {
    $lis.removeClass(attribute_name);
    $(pieces).attr(attribute_name, false);
    if (piece_index != null) {
      $lis.eq(piece_index).addClass(attribute_name);
      pieces[li_index_to_piece_index(piece_index)][attribute_name] = true;
    }
    draw_piece_tree();
  }
  
  /**
  * Trigger canvas animation on player row resize
  * If animation is already in progress, disable the step callback for extra-graceful animation!
  */
  var animating = false;
  function animate_player_row(piece_index, row_height) {
    var animation_params = {duration: ANIMATION_DURATION};
    if (!animating) {
      animating = true;
      $.extend(animation_params, {step: draw_piece_tree});
      setTimeout(function() {
        animating = false;
      }, ANIMATION_DURATION);
    }
    $(renderer.get_row_offset_height(piece_index)).animate({offset: row_height}, animation_params);
  }
  
  // util
  function li_index_to_piece_index(li_index) {
    return pieces.length-li_index-1;
  }
};

/**
* Renderer turns high-level drawing commands into pixel-based <canvas> render commands
*/
var PLAYER_OFFSET_HEIGHT = '42px'; // global so player.js can get it too
jQuery.PieceTree.Renderer = function(canvas) {
  // coordinate constants
  var ROW_HEIGHT = 82;
  var COL_WIDTH = 33;
  var X_ORIGIN = 95;
  var y_origin = 0; // nuts this one is not constant, it turns out.
  var CANVAS_WIDTH = 120;
  var canvas_height = 0;
  
  // init
  var row_offset_heights = []; // populated by $.PieceTree.Renderer.init_row_heights()
  var ctx = canvas.getContext('2d');
  
  var public_methods = {
    /**
    * Entrypoint. Convert coordinates & issue low-level drawing commands. (Order matters, for Z-axis.)
    */
    draw_tree: function(piece_tree) {
      init_coords(piece_tree.dots.length);
      
      draw_background();
      jQuery.each(piece_tree.dots, function() {
        draw_horiz_line(grid_pos_to_location(this), this.highlight);
      });
      jQuery.each(piece_tree.lines, function() {
        draw_line(grid_pos_to_location(this.from), grid_pos_to_location(this.to),
          (this.to.row/piece_tree.dots.length), this.highlight);
      });
      jQuery.each(piece_tree.dots, function() {
        draw_dot(grid_pos_to_location(this), this.highlight);
      });
    },
    
    init_row_heights: function(num_pieces) {
      for (var i=0; i<num_pieces; i++) row_offset_heights.push({offset: 0});
    },
    
    get_row_offset_height: function(piece_index) {
      return row_offset_heights[piece_index];
    },
    
    mouse_region_piece_index: function(mouse_position) {
      return parseInt((canvas_height-mouse_position.y)/ROW_HEIGHT)
    }
  };
  
  
  /**
  * 'private' drawing & utility methods
  */
  
  // Resize canvas and set up y axis
  function init_coords(num_pieces) {
    canvas_height = num_pieces * ROW_HEIGHT + row_offset_sum(num_pieces) + 8; // match up with li margin
    y_origin = canvas_height - (ROW_HEIGHT / 2);
    $(canvas).attr('height', canvas_height);
  }
  
  function draw_dot(center, highlight) {
    var radius = 4;
    ctx.beginPath();
    ctx.arc(center.x, center.y, radius, 0, Math.PI*2, true);
    ctx.strokeStyle = highlight ? '#B0A095' : '#C3B7AE';
    ctx.lineWidth = 3;
    ctx.stroke();
    if ($.browser.msie) ctx.arc(center.x, center.y, radius, 0, Math.PI*2, true); // excanvas makes us redraw
    ctx.fillStyle = 'rgb(250, 250, 250)';
    ctx.fill();
  }
  
  function draw_line(start, end, recency, highlight) {
    // curve shape
    var CURVE_WIDTH_COEFF = 17; // high numbers make wider curves (0 is straight line)
    var CURVE_END_THETA_COEFF = 0.006 ; // _slightly_ higher numbers ease curve at top for leftward-lurching curves
    var theta = Math.PI*0.75;
    var magnitude = Math.log((start.y-end.y)*0.05+1.0)*CURVE_WIDTH_COEFF;
    var start_control_point = vector_end_point({theta: theta, magnitude: magnitude}, start);
    var end_control_point = vector_end_point({theta: -(theta - (start.x-end.x)*CURVE_END_THETA_COEFF), magnitude: magnitude}, end);
    var curve_width = (start.x - start_control_point.x); // for gradient
    
    // gradient fill
    var GRADIENT_EXTRA_WIDTH = 60; // higher numbers make leftmost lines fade less
    var GRADIENT_PULL_RIGHT_COEFF = 3.5; // higher numbers make OLD lines fade more
    var gradient_right_x = start.x + (Math.exp((1-recency)*2.5) * GRADIENT_PULL_RIGHT_COEFF);
    var gradient = ctx.createLinearGradient(
      gradient_right_x-(curve_width+GRADIENT_EXTRA_WIDTH), 0,
      gradient_right_x, 0);
    gradient.addColorStop(0.0, highlight ? '#d7a' : '#fff');
    gradient.addColorStop(1.0, '#E33C6F');
    if (!$.browser.msie) ctx.strokeStyle = gradient;
    else ctx.strokeStyle = '#E33C6F'; // excanvas can't handle it
    ctx.lineWidth = 5;
    
    ctx.beginPath();
    ctx.moveTo(start.x, start.y);
    ctx.bezierCurveTo(
      start_control_point.x, start_control_point.y,
      end_control_point.x, end_control_point.y,
      end.x, end.y);
    ctx.stroke();
  }
  
  function draw_horiz_line(center, highlight) {
    ctx.beginPath();
    ctx.strokeStyle = highlight ? 'rgb(153, 153, 136)' : '#C3B7AE';
    ctx.lineWidth = 2;
    ctx.moveTo(center.x, center.y);
    ctx.lineTo(center.x+100, center.y);
    ctx.stroke();
  }
  
  function draw_background() {
    var gradient = ctx.createLinearGradient(0, 0, CANVAS_WIDTH, 0);
    gradient.addColorStop(0.0, '#f9f9f9');
    gradient.addColorStop(1.0, '#fff');
    ctx.fillStyle = gradient;
    if (!$.browser.msie) ctx.fillRect(0, 0, CANVAS_WIDTH, canvas_height);
    else ctx.clearRect(0, 0, CANVAS_WIDTH, 0); // ignores gradient angle so forget it
  }
  
  /**
  * Map the row/col gridspace to x/y coords, taking into account the spacewarp of piece player.
  * cols start counting at 1, rows at 0!
  */
  function grid_pos_to_location(grid_pos) {
    return {
      x: X_ORIGIN+COL_WIDTH-grid_pos.col*COL_WIDTH,
      y: y_origin-grid_pos.row*ROW_HEIGHT - row_offset_sum(grid_pos.row+1)};
  }
  
  function row_offset_sum(num_pieces) {
    return $(row_offset_heights).slice(0, num_pieces).map(function() {
      return this.offset;
    }).sum();
  }
  
  function vector_end_point(vector, start_point) {
    return {
      x: start_point.x + vector.magnitude * Math.cos(vector.theta),
      y: start_point.y - vector.magnitude * Math.sin(vector.theta)
    };
  }
  
  return public_methods;
};
