var jed = (function (jed) {

    //////// Models

    //// Link
    
    var Link = jed.Link = function (elem) {
      this.element = elem;
      this.href = elem.getAttribute('href');
      // Expectation: If link's inner HTML is not just text, it is an img tag
      if (/^<img [^>]+>$/.test(elem.innerHTML))
        this.text = elem.getAttribute('title') || elem.children[0].getAttribute('alt');
      else
        this.text = elem.getAttribute('title') || elem.innerText || elem.textContent;
      // Replace non-ASCII chars to hash in accordance with Ruby
      for (entity in jed.unescapePairs) this.text = this.text.replace(jed.unescapePairs[entity], entity);
      // Parse frequency name from element's classes
      this.frequencyName = elem.className.match(RegExp(jed.frequencyNames.join('|')))[0];
      this.percentile = elem.getAttribute('data-percentile');
      this.originalOffset = jed.offset(elem);
      elem.model = this;
    };

    Link.prototype.click = function () {
      if (this._clicked || !this.hid) return false;
      jed.ajax({
        url : 'heatsink.php',
        data : {id : this.hid}
      });
      return this._clicked = true;
    };

    Link.prototype.toggleVisibility = function (force) {
      var style = this.element.style;
      if (typeof force !== 'undefined')
        style.visibility = force ? 'visible' : 'hidden';
      else
        style.visibility = style.visibility === 'hidden' ? 'visible' : 'hidden';
    };

    Link.prototype.toItem = function () {
      var node = this.element.cloneNode(true);
      node.innerHTML = this.text;
      node.original = this;
      return node;
    };

    //// LinkCollection

    var LinkCollection = jed.LinkCollection = function (linksArray) {
      this.links = linksArray || [];
      this.equivalents = {};
    };

    LinkCollection.prototype.append = function (linkElem) {
      var linkObj = new Link(linkElem);
      var hrefAndText = linkObj.href + linkObj.text;
      if (this.equivalents[hrefAndText] && this.equivalents[hrefAndText].length) {
        this.equivalents[hrefAndText].push(linkObj);
      } else {
        this.equivalents[hrefAndText] = [linkObj];
      }
      linkObj.hid = hex_sha1(hrefAndText + this.equivalents[hrefAndText].length);
      this.links.push(linkObj);
    };
    
    LinkCollection.prototype.toggleVisibility = function (force) {
      this.forEach(function (link) { link.toggleVisibility(force); });
    };

    LinkCollection.prototype.forEach = function (fn) {
      jed.forEach(this.links, fn);
    };

    LinkCollection.prototype.get = function (index) {
      return this.links[index];
    };

    LinkCollection.prototype.size = function () {
      return this.links.length;
    };

    LinkCollection.prototype.withFrequency = function (frequencyName) {
      return new LinkCollection(jed.filter(this.links, function (link) { return link.frequencyName === frequencyName }));
    };

    LinkCollection.fromElements = function (elements) {
      var collection = new this();
      for (var i = 0; i < elements.length; i++) collection.append(elements[i]);
      return collection;
    };

    //// Frequency

    var Frequency = jed.Frequency = function (elem) {
      this.name = elem.getAttribute('id');
      this.element = elem;
      elem.model = this;
    };

    //////// Controller

    var LinkController = jed.LinkController = {
      'click' : function (e) {
        this.model.click();
      }
    };

    var FrequencyController = jed.FrequencyController = {
      'click' : function (e) {
        jed.toggleExplanation(true);
        // let's not mess things up and do one thing after the other
        if (jed.busy()) return;
        $(this).toggleClass('activated');
        // click on active frequency removes list
        if (jed.activeFrequencyList && jed.activeFrequencyList.frequency === this.model) {
          jed.activeFrequencyList.destroy();
          delete jed.activeFrequencyList;
        // click on non-active frequency while other frequency active
        } else if (jed.activeFrequencyList) {
          jed.activeFrequencyList.destroy(this.model.links.size());
          jed.activeFrequencyList = new FrequencyListView(this.model, this.parentNode);
        // no currently active frequency
        } else {
          jed.activeFrequencyList = new FrequencyListView(this.model, this.parentNode);
        }
      }
    };

    //////// Views

    //// FrequencyListView
    
    var FrequencyListView = jed.FrequencyListView = function (frequency, insertAfter) {
      var self = this;
      this.element = document.createElement('ol');
      this.items = [];
      this.frequency = frequency;
      var itemNum = frequency.links.size();
      // global transition notifier
      jed.busy(itemNum);
      for (var i = 0; i < itemNum; i++)
        this.append(new FrequencyListItemView(frequency.links.get(i)));
      // insert ordered list containing empty items
      this.element.style.display = 'none';
      insertAfter.parentNode.insertBefore(this.element, insertAfter.nextSibling);
      $(this.element).show(250, function () {
        // place moving links
        jed.forEach(self.items, function (item) { item.place() });
        self.frequency.links.toggleVisibility();
        jed.forEach(self.items, function (item) { item.move() });
      });
    };

    FrequencyListView.prototype.append = function (listItem) {
      this.items.push(listItem);
      this.element.appendChild(listItem.listElement);
    };

    FrequencyListView.prototype.destroy = function (nextSize) {
      var olElem = this.element,
          olElemHeight = jed.height(olElem),
          itemNum = this.items.length;
      jed.busy(itemNum);
      jed.forEach(this.items, function (item) { item.move(olElemHeight, itemNum, nextSize); });
      // remove ol element immediately if it is replaced by new one
      if (nextSize)
        olElem.parentNode.removeChild(olElem);
      // attach ol element to body to animate its death if there is no successor
      else {
        var olOffset = $(olElem).offset();
        $(document.body).append($(olElem).css(
          {'left': olOffset.left, 'top': olOffset.top, 'position': 'absolute'}
        ).hide(154, function () { olElem.parentNode.removeChild(olElem); }));
      }
      $(this.frequency.element).removeClass('activated');
    };

    //// FrequencyListItemView

    var FrequencyListItemView = jed.FrequencyListItemView = function (link) {
      this.listElement = document.createElement('li');
      this.linkElement = link.toItem();
      this.inList      = false;
      this.inTransit   = false;
    };

    FrequencyListItemView.prototype.move = function (currListHeight, currItemNumber, nextItemNumber) {
      var self = this;
      if (this.inTransit) return;
      this.inTransit = true;
      $(this.linkElement).animate(
        this.goalOffset(currListHeight, currItemNumber, nextItemNumber),
        404,
        'linear',
        function () {
          if (self.inList) {
            self.linkElement.original.toggleVisibility();
            document.body.removeChild(self.linkElement);
          }
          else {
            self.inList = !self.inList;
          }
          jed.busy(-1);
          self.inTransit = false;
        }
      );
    };

    FrequencyListItemView.prototype.goalOffset = function (currListHeight, currItemNumber, nextItemNumber) {
      var offset;
      if (this.inList) {
        offset = jed.offset(this.linkElement.original.element);
        if (nextItemNumber && currItemNumber !== nextItemNumber) {
          offset.top -= (currItemNumber - nextItemNumber) * jed.height(this.listElement);
        } else if (!nextItemNumber) {
          offset.top -= currListHeight - jed.height(this.listElement);
         }
      } else {
        offset = jed.offset(this.listElement);
      }
      return offset;
    };

    FrequencyListItemView.prototype.place = function (numberOfPixels) {
      var style = this.linkElement.style;
      var startPos = jed.offset(this.linkElement.original.element);
      style['position'] = 'absolute';
      style['left']     = startPos['left'] + 'px';
      style['top']      = startPos['top'] + 'px';
      document.body.appendChild(this.linkElement);
    };

    //////// Helpers

    jed.filter = function (arr, filterFn, thisp) {
      var newArr = [];
      for (var i = 0; i < arr.length; i++) {
        if (i in arr) {
          var value = arr[i];
          if (filterFn.call(thisp, value, i, arr)) newArr.push(value);
        }
      }
      return newArr;
    };

    jed.forEach = function(arr, forEachFn, thisp) {
      for (var i = 0; i < arr.length; i++) {
        if (i in arr) forEachFn.call(thisp, arr[i], i, arr);
      }
    };

    jed.map = $.map;

    jed.height = function (elem) {
      return $(elem).outerHeight(true);
    };

    jed.offset = function (elem) {
      var offset = $(elem).offset();
      // repair offset for wrapping inline elements, where left offset is the
      // leftmost position of the element, not its left position at its top
      if (offset.left === jed.leftmostOffset) {
        offset.left = elem.offsetLeft;
      }
      return offset;
    };

    jed.unescapePairs = {
      '&auml;' : /ä/g, '&Auml;' : /Ä/g,
      '&ouml;' : /ö/g, '&Ouml;' : /Ö/g,
      '&uuml;' : /ü/g, '&Uuml;' : /Ü/g,
      '&szlig;' : /ß/g,
      '&rsquo;' : /’/g
    };

    // Alias jQuery's Ajax fn
    jed.ajax = $.ajax;

    // Setup fn for interactive parts
    jed.setup = function () {
      // Determine leftmost offset
      jed.leftmostOffset = $('body > p:first-child').offset().left;

      jed.frequencies = jed.map($('#legend dt'), function (freqDt) { return new jed.Frequency(freqDt); });
      jed.frequencyNames = jed.map(jed.frequencies, function (freq) { return freq.name; });
      // Build global link collection
      jed.links = LinkCollection.fromElements(
        $(jed.map(jed.frequencyNames, function (freqName) { return 'a.' + freqName; }).join(', ')).toArray()
      );
      // Split link collection into frequencies
      jed.forEach(jed.frequencies, function (freq) { freq.links = jed.links.withFrequency(freq.name); });

      // Set up event listeners
      // DTs
      $(jed.map(jed.frequencies, function (freq) { return freq.element; })).addClass(
        'interactive'
      ).click(FrequencyController.click);
      // DDs
      $('#legend dd').addClass(
        'interactive'
      ).click(
        function () { FrequencyController.click.call(this.previousElementSibling); }
      ).hover(
        function () { $(this.previousElementSibling).toggleClass('hover'); }
      );
      // Links
      jed.links.forEach(function (link) { $(link.element).click(LinkController.click) });
      // Legend explanation
      $("#legend").hover(jed.toggleExplanation);

      // Color icons
      $(jed.map(jed.frequencyNames, function (freqName) { return 'a.' + freqName + ' img'; }).join(', ')).each(jed.colorIcon);
    };

    jed.toggleExplanation = function (force) {
      if (force === true || jed.listWasActivated) {
        jed.listWasActivated = true;
        return jed.explanation && jed.explanation.hide();
      }
      if (jed.explanation) {
        return jed.explanation.toggle();
      }
      // only runs once
      var verticalPos = $(this).position().top + parseInt($(this).height()/2, 10);
      var horizontalPos = $(this).position().left + $(this).width();
      var explanation = jed.explanation = $('<p id="explanation">The links&rsquo; colors tell you about their popularity, determined by how often they are being clicked on compared to the other links. Click on any of the categories to the left and see a list of all associated links.</p>').css(
        { 'position': 'absolute', 'top': verticalPos, 'left': horizontalPos }
      );
      $(document.body).append(explanation);
    };

    jed.colorIcon = function () {
      // Set dimenions
      var x = 0, y = 0, width = 16, height = 16,
          rgb = $(this).parent().css('color').match(/\d+/g),
          canvas = document.createElement('canvas'),
          context;
      // Give up if canvas is not supported and just leave the images be
      if (!canvas.getContext) return;
      canvas.width = width, canvas.height = height;
      context = canvas.getContext('2d');
      // Give up if drawImage is not supported
      if (!context.drawImage) return;
      
      // Wait for image to load
      $(this).load(function () {
        // Put icon on canvas
        context.drawImage(this, x, y);
        // Get two copies: 1 for color, 1 for b/w
        var colorImgData = context.getImageData(x, y, width, height);
        var bwImgData = context.getImageData(x, y, width, height);
        var pix = bwImgData.data;

        // Loop over each pixel and compute hue.
        for (var i = 0, n = pix.length; i < n; i += 4) {
          var luminosity = (pix[i] * 0.3 + pix[i+1] * 0.59 + pix[i+2] * 0.11) / 255;
          pix[i  ] = rgb[0] * luminosity;
          pix[i+1] = rgb[1] * luminosity;
          pix[i+2] = rgb[2] * luminosity;
        }

        // Draw the ImageData object
        context.putImageData(bwImgData, x, y);

        $(this).replaceWith(canvas);

        // On mouseover display original color version
        $(canvas).hover(
          function () { context.putImageData(colorImgData, x, y); },
          function () { context.putImageData(bwImgData, x, y); }
        );
    });
    };

    // counts tasks left on stack, app is not busy if stack is empty
    jed.busy = (function () {
      var stack = 0;
      return function (tasks) {
        if (typeof tasks === 'number' && parseInt(tasks, 10) === tasks)
          stack += tasks;
        if (stack < 0)
          stack = 0;
        return stack !== 0;
      };
    })();

    return jed;

})({});

$(document).ready(function () {
  jed.setup();
});

