MN.Relation = Class.create();

// An Array of all current instances of MN.Relation.
MN.Relation.instances = [ ];

// Some arrays for easy lookups based on relation id.
MN.Relation.fromRelations = [ ];
MN.Relation.toRelations   = [ ];

// Create the canvas element after a successful link drop.
MN.Relation.insertNewRelation = function(xhr) {
  var response = eval('(' + xhr.responseText + ')');
  var relationId = 'relation.' + response.id;

  var n = Builder.node('canvas',
    {
      className : 'relation',
      id        : relationId,
      width     : 20,
      height    : 20
    }
  );

  var b = Builder.node('span',
    {
      className : 'begin point',
      id        : 'begin.' + response.id
    },
    'o'
  );

  var e = Builder.node('span',
    {
      className : 'end point',
      id        : 'end.' + response.id
    },
    'o'
  );

  var workspace = $$('div.workspace').first();

  workspace.appendChild(n);
  workspace.appendChild(b);
  workspace.appendChild(e);

  var relation = new MN.Relation(
    $(relationId),
    {
      from : response.from_note_id,
      to   : response.to_note_id
    }
  );

  // FIXME - Way too much fucking code.
  var fn = MN.Note.instances[relation.from];
  var tn = MN.Note.instances[relation.to];
  fn.addToFromRelations(relation);
  tn.addToToRelations(relation);
  fn.edgeNeedsUpdate = true;
  tn.edgeNeedsUpdate = true;
  fn.organizeRelations();
  tn.organizeRelations();
  relation.draw();
}

// Create a new Relation between 2 Notes by dropping one Note's linker
// on top of another Note.
MN.Relation.createOnDrop = function(linker, note) {
  var fromId = MN.id(linker);
  var toId   = MN.id(note);
  var fn     = MN.Note.instances[fromId];
  var tn     = MN.Note.instances[toId];
  fn.edgeNeedsUpdate = true;
  tn.edgeNeedsUpdate = true;
  if (fromId != toId) {
    // alert('Creating Relation from ' + fromId + ' to ' + toId + '.');
    var url = '/relation/create'
    new Ajax.Request (
      url,
      {
        postBody  : 'from_id=' + fromId + '&to_id=' + toId,
        onSuccess : MN.Relation.insertNewRelation
      }
    );
  }
}

MN.Relation.prototype = {

  // attributes

  id        : null,
  element   : null,

  // numeric id of notes
  from      : null,
  to        : null,

  // top, bottom, left, right
  fromEdge  : null,
  toEdge    : null,

  // integer from 0 .. * indicating where this point fits on an edge
  fromIndex : null,
  toIndex   : null,
  
  // TODO - wiggle room that I'm adding in later so that there's room to
  // draw everything that needs to be drawn.
  canvasBorder : 4,

  // methods

  // constructor
  initialize: function(element, options) {
    Object.extend(this, options);

    this.element = element;

    var id = MN.id(element);
    this.id = id;
    MN.Relation.instances[id] = this;

    // The purpose of these two arrays is to make it easy
    // for the notes to find the relations that belong to
    // them when the MN.Note objects get constructed.
    var r;
    if (r = MN.Relation.fromRelations[this.from]) {
      r.push(this);
    } else {
      MN.Relation.fromRelations[this.from] = [ this ];
    }
    if (r = MN.Relation.toRelations[this.to]) {
      r.push(this);
    } else {
      MN.Relation.toRelations[this.to] = [ this ];
    }

    Event.observe($('end.' + id), 'click', 
      this.makeScrollingFunction(this.from)
    ); 
    Event.observe($('begin.' + id), 'click', 
      this.makeScrollingFunction(this.to)
    );
  },

  // make scrolling function
  makeScrollingFunction: function(noteId) {
    return function(event) {
      var note       = MN.Note.instances[noteId];
      var cx         = parseInt(window.innerWidth / 2);
      var cy         = parseInt(window.innerHeight / 2);
      var dimension  = note.dimension();
      var position   = note.position();
      var offsetX    = -cx + parseInt(dimension.width  / 2);
      var offsetY    = -cy + parseInt(dimension.height / 2);
      // console.log(offsetX, offsetY);
      // console.log(position.toSource());
      offsetX = offsetX + position.x > 0 ? offsetX : -position.x;
      offsetY = offsetY + position.y > 0 ? offsetY : -position.y;
      // TODO - Still need to take Page width into consideration.
      new Effect.ScrollToXY(note.element, {
        offsetX: offsetX,
        offsetY: offsetY
      })
      new Effect.Highlight(note.element, { queue: 'end', startcolor: '#ffffff' });
    }.bind(this);
  },

  // Find the note edges that this relation is connected to.
  // Also save this info to this.
  findNoteEdges: function() {
    var edges = this._findNoteEdges();
    if (edges) {
      this.fromEdge = edges.from;
      this.toEdge   = edges.to;
    }
    return edges;
  },

  // Do the actual work of finding edges.
  _findNoteEdges: function() {
    var fn = MN.Note.instances[this.from];
    var tn = MN.Note.instances[this.to];
    var fp = fn.midpoint();
    var tp = tn.midpoint();

    if (fp.x == tp.x && fp.y == tp.y) {
      // there's no point in drawing arrows
      // if they perfectly overlap with each other.
      return null;
    } else if (fp.x == tp.x) {
      // straight line
      // - top|bottom
      if (fp.y < tp.y) {
        return {
          from : 'bottom',
          to   : 'top'
        };
      } else {
        return {
          to   : 'bottom',
          from : 'top'
        };
      }
    } else if (fp.y == tp.y) {
      // straight line
      // - left|right
      if (fp.x < tp.x) {
        return { from: 'right', to: 'left' };
      } else {
        return { to: 'right', from: 'left' };
      }
    } else {
      // arbitrary combination
      // - top|right|bottom|left

      var result;
      var line = [ fp, tp ];
      var edges = {
        from : null,
        to   : null
      };

      // FROM

      if (fp.x < tp.x) {
        // check fn.rightEdge()
        result = MN.getLineIntersection(line, fn.rightEdge());
        if (result.isIntersecting) {
          edges.from = 'right';
        } else {
          if (fp.y < tp.y) {
            result = MN.getLineIntersection(line, fn.bottomEdge());
            if (result.isIntersecting) {
              edges.from = 'bottom';
            } else {
              return null;
            }
          } else {
            result = MN.getLineIntersection(line, fn.topEdge());
            if (result.isIntersecting) {
              edges.from = 'top';
            } else {
              return null;
            }
          }
        }
      } else {
        result = MN.getLineIntersection(line, fn.leftEdge());
        if (result.isIntersecting) {
          edges.from = 'left';
        } else {
          if (fp.y < tp.y) {
            result = MN.getLineIntersection(line, fn.bottomEdge());
            if (result.isIntersecting) {
              edges.from = 'bottom';
            } else {
              return null;
            }
          } else {
            result = MN.getLineIntersection(line, fn.topEdge());
            if (result.isIntersecting) {
              edges.from = 'top';
            } else {
              return null;
            }
          }
        }
      }

      // TO

      // process of elimination
      var lastOneStanding = {
        'top'  : tn.topEdge(),
        right  : tn.rightEdge(),
        bottom : tn.bottomEdge(),
        left   : tn.leftEdge()
      };

      // We can eliminate one edge immediately, because
      // it's not sane to connect fn.from to tn.from
      delete lastOneStanding[edges.from];

      // 3 edges remain to be checked.
      // http://encytemedia.com/blog/articles/2005/12/07/prototype-meets-ruby-a-look-at-enumerable-array-and-hash
      $H(lastOneStanding).each(function(eg) {
        // A sanity check here can save us from checking
        // one more of the edges such that we'd only have to
        // call MN.getLineIntersection a maximum of 2 times.
        result = MN.getLineIntersection(line, eg.value);
        if (result.isIntersecting) {
          edges.to = eg.key;
        } else {
          delete lastOneStanding[eg.key];
        }
      });

      return edges;
    }
  },
  
  // reposition the linking points at the end of each relation
  adjustPoints: function(x1, y1, x2, y2) {
    var offsets = Position.positionedOffset(this.element);
    var begin   = $('begin.' + this.id);
    var end     = $('end.'   + this.id);
    if (begin) {
      Element.setStyle($('begin.' + this.id), {
        left : (offsets[0] + x1) + 'px',
        top  : (offsets[1] + y1) + 'px'
      });
    }
    if (end) {
      Element.setStyle($('end.' + this.id), {
        left : (offsets[0] + x2) + 'px',
        top  : (offsets[1] + y2) + 'px'
      });
    }
  },

  // Resize and reposition canvas so that the arrow can be drawn.
  adjustCanvas: function() {
    var el   = this.element;
    var from = this.fromPoint();
    var to   = this.toPoint();

    // prepare to resize
    var wd;
    var hi;
    if (from.x > to.x) {
      wd = from.x - to.x;
    } else {
      wd = to.x - from.x;
    }
    if (from.y > to.y) {
      hi = from.y - to.y;
    } else {
      hi = to.y - from.y;
    }

    // prepare to reposition
    var x;
    var y;
    if (from.x > to.x) {
      x = to.x;
    } else {
      x = from.x;
    }
    if (from.y > to.y) {
      y = to.y;
    } else {
      y = from.y;
    }

    // resize and reposition
    el.width  = wd;  // firefox wants to use element width
    el.height = hi;  // and height
    Element.setStyle(
      el,
      {
        left   : x  + 'px',
        top    : y  + 'px',
        width  : wd + 'px',  // safari wants to use css width
        height : hi + 'px'   // and height
      }
    );
  },

  draw: function() {
    this.adjustCanvas();
    var el = this.element;
    var ctx = el.getContext('2d');
    var wd = Element.getStyle(el, 'width');
    var hi = Element.getStyle(el, 'height');
    wd = parseInt(wd.replace('px', ''));
    hi = parseInt(hi.replace('px', ''));
    ctx.clearRect(0, 0, wd, hi);
    ctx.lineWidth   = 2.0;
    ctx.strokeStyle = "#808080";

    var from = this.fromPoint();
    var to   = this.toPoint();
    var x1, y1, x2, y2;
    if ((from.x < to.x) && (from.y < to.y)) {
      // from \ to
      x1 = 0;
      y1 = 0;
      x2 = wd;
      y2 = hi;
    } else if ((from.x > to.x) && (from.y < to.y)) {
      // to / from
      x1 = wd;
      y1 = 0;
      x2 = 0;
      y2 = hi;
    } else if ((from.x > to.x) && (from.y > to.y)) {
      // to \ from
      x1 = wd;
      y1 = hi;
      x2 = 0;
      y2 = 0;
    } else if ((from.x < to.x) && (from.y > to.y)) {
      // from / to
      x1 = 0;
      y1 = hi;
      x2 = wd;
      y2 = 0;
    }
    this.adjustPoints(x1, y1, x2, y2);

    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
  },

  // this is the coordinate where the arrow will begin from
  fromPoint: function() {
    var fn = MN.Note.instances[this.from];
    if (fn.edgeNeedsUpdate) {
      fn.organizeRelations();
    }

    // TODO - evenly distribute points along edge when multiple relations
    // share an edge.
    var point = { x: 0, y: 0 };
    if (this.fromEdge == 'top') {
      var count      = fn.relationsByEdge['top'].length;
      var length     = count * 12;
      var topEdge    = fn.topEdge();
      var width      = topEdge[1].x - topEdge[0].x;
      var index      = this.fromIndex;
      point.x        = topEdge[0].x + parseInt((width - length) / 2) + (12 * index);
      point.y        = topEdge[0].y;
    } else if (this.fromEdge == 'right') {
      var count      = fn.relationsByEdge['right'].length;
      var length     = count * 12;
      var rightEdge  = fn.rightEdge();
      var height     = rightEdge[1].y - rightEdge[0].y;
      var index      = this.fromIndex;
      point.x        = rightEdge[0].x;
      point.y        = rightEdge[0].y + parseInt((height - length) / 2) + (12 * index);
    } else if (this.fromEdge == 'bottom') {
      var count      = fn.relationsByEdge['bottom'].length;
      var length     = count * 12;
      var bottomEdge = fn.bottomEdge();
      var width      = bottomEdge[1].x - bottomEdge[0].x;
      var index      = this.fromIndex;
      point.x        = bottomEdge[0].x + parseInt((width - length) / 2) + (12 * index);
      point.y        = bottomEdge[0].y;
    } else if (this.fromEdge == 'left') {
      var count      = fn.relationsByEdge['left'].length;
      var length     = count * 12;
      var leftEdge   = fn.leftEdge();
      var height     = leftEdge[1].y - leftEdge[0].y;
      var index      = this.fromIndex;
      point.x        = leftEdge[0].x;
      point.y        = leftEdge[0].y + parseInt((height - length) / 2) + (12 * index);
    } else {
      alert('problem');
    }
    return point;
  },

  // this is the coordinate where the arrow will end at.
  toPoint: function() {
    var tn = MN.Note.instances[this.to];
    if (tn.edgeNeedsUpdate) {
      tn.organizeRelations();
    }

    // TODO - evenly distribute points along edge when multiple relations
    // share an edge.
    var point = { x: 0, y: 0 };
    if (this.toEdge == 'top') {
      var count      = tn.relationsByEdge['top'].length;
      var length     = count * 12;
      var topEdge    = tn.topEdge();
      var width      = topEdge[1].x - topEdge[0].x;
      var index      = this.toIndex;
      point.x        = topEdge[0].x + parseInt((width - length) / 2) + (12 * index);
      point.y        = topEdge[0].y;
    } else if (this.toEdge == 'right') {
      var count      = tn.relationsByEdge['right'].length;
      var length     = count * 12;
      var rightEdge  = tn.rightEdge();
      var height     = rightEdge[1].y - rightEdge[0].y;
      var index      = this.toIndex;
      point.x        = rightEdge[0].x;
      point.y        = rightEdge[0].y + parseInt((height - length) / 2) + (12 * index);
    } else if (this.toEdge == 'bottom') {
      var count      = tn.relationsByEdge['bottom'].length;
      var length     = count * 12;
      var bottomEdge = tn.bottomEdge();
      var width      = bottomEdge[1].x - bottomEdge[0].x;
      var index      = this.toIndex;
      point.x        = bottomEdge[0].x + parseInt((width - length) / 2) + (12 * index);
      point.y        = bottomEdge[0].y;
    } else if (this.toEdge == 'left') {
      var count      = tn.relationsByEdge['left'].length;
      var length     = count * 12;
      var leftEdge   = tn.leftEdge();
      var height     = leftEdge[1].y - leftEdge[0].y;
      var index      = this.toIndex;
      point.x        = leftEdge[0].x;
      point.y        = leftEdge[0].y + parseInt((height - length) / 2) + (12 * index);
    }
    return point;
  },

  // How do you destroy DOM elements?
  destroy: function() {
    var beginId = 'begin.' + this.id;
    var endId   = 'end.'   + this.id;
    delete(MN.Relation.instances[this.id]);
    new Effect.Fade(this.element);
    new Effect.Fade($(beginId));
    new Effect.Fade($(endId));
  },

  ____: "the end"
};

// vim:expandtab sw=2 sts=2
