/*
 * Chump Asynchronous Keyword Explorer
 *
 * $Id: $
 */

// Clobber the onload handler
window.onload = function() { Cake.initDocument() };

var Cake = {

  ////
  // Constants

  /* Maximum number of results to display */
  _MAX_RESULTS : 5,

  /* Prefix for search URL. Keyword will be appended */
  _SEARCH_URL_PREFIX : "/tag/",

  /* Prefix for links generated by search results. URI stub is appended to this */
  _LINK_PREFIX : "/",

  /////////////////
  // Public members

  // Searches for entries related to a keyword and displays them
  // anchor -  The keyword's <a> element
  show : function (anchor) {

    var anchor = this;

    // If another window is focused, do nothing
    if (!this.cake._bodyFocused) {
      return false;
    }

    // Record the keyword anchor that we're working with
    var anchorId = this.cake._getAnchorId(anchor);
    this.cake._currentAnchorId = anchorId;

    // What keyword are we looking for?
    var keyword = anchor.firstChild.data;
    
    // Lazily instantiate result container
    if (!this.cake._resultPane) {
      this.cake._createResultPane();
    }

    // Checked for cached results
    if (this.cake._cachedHtml[anchorId]) {

      this.cake._displayCachedHtml(anchor);

    } else if (this.cake._cachedResults[keyword]) {

      this.cake._displayCachedResults(anchor);
      
    } else {

      this.cake._displaySearchResults(anchor);
    }
    return false;
  },

  // Begins a timer that will cause search results to be hidden
  hide : function () {
    this._currentAnchorId = null;

    if(this._resultPane) {

      this._clearHideTimer();
      this._clearFadeInterval();
      this._hideTimerId = window.setTimeout(this._beginFade,500);
    }
  },

  // Callback when window gains input focus
  bodyFocus : function () {
    Cake._bodyFocused = true;
  },

  // Callback when window loses input focus
  bodyBlur : function () {
    Cake._bodyFocused = false;
    Cake.hide();
  },

  // Paints event handlers and metadata onto keyword anchors
  // Assumes a lot about the structure of the host document
  initDocument : function() {

    document.body.onfocus=this.bodyFocus;
    document.body.onblur=this.bodyBlur;

    // Paint mouseevents onto keywords
    var allDivs = document.body.getElementsByTagName("div");
    for (var I=0 ; I < allDivs.length ; I++) {

      if (allDivs[I].className == "item") {

        var item = allDivs[I];
        var timestamp = item.id.substring(1);
        var byline = item.getElementsByTagName("div")[1];
        var anchors = byline.getElementsByTagName("a");
        for (var J=1 ; J < anchors.length ; J++) {
          var keyword = anchors[J];
          keyword.onmouseout = this.hide;
          keyword.style.display = "inline-block";

          keyword.timestamp = timestamp;
          keyword.onmouseover = this.show;
          keyword.cake = this;
        }
      }
    }
  },


  ////
  // Pseudo-private members, denoted by leading underscore
  //

  /* The anchor that invoked the current search */
  _currentAnchorId : null,

  /* Div to place results in */
  _resultPane : null,

  /* Cache of results of previous searches, keyed by keyword */
  _cachedResults : new Array(),

  /* Cache of HTML from previous searches, keyed by keyword anchor id */
  _cachedHtml : new Array(),

  /* Whether the window is focused */
  _bodyFocused : true,

  /* Opacity of results pane - starting at 100 causes flicker in gecko */
  _paneAlpha : 99,

  /* IDs for timers */
  _hideTimerId : null,
  _fadeIntervalId : null,


  // Performs a search against lucene for a keyword, 
  // and displays results
  // anchor - The keyword's <a> element
  _displaySearchResults : function (anchor) {

    var keyword = anchor.firstChild.data;
    var anchorId = this._getAnchorId(anchor);

    var req = this._getHttpReq();
    if (!req) {
      // XmlHttp not supported
      return;
    }

    // Set progress style on anchor
    this._setProgressStyles(anchor,true);    

    var url=this._SEARCH_URL_PREFIX+escape(keyword)+"?format=xml";

    // Record on the request which keyword it's searching for
    req.open("GET",url,true);
    req.onreadystatechange=function() {

      // When request completes successfully
      if ((req.readyState==4)&&(req.status==200)) {

        // Unhook this handler (bug in some Opera versions
        // keeps calling on readyState == 4
        delete req.onreadystatechange;
          
        // Process and cache the search results
        anchor.cake._cacheSearchResults(keyword,req.responseXML);

        // Unset progress style on anchor
        anchor.cake._setProgressStyles(anchor, false);

        // If Cake is still the current search
        if (anchorId == anchor.cake._currentAnchorId) {

          // Show the search results
          anchor.cake._displayCachedResults.call(anchor.cake,anchor);
        } 
      }
    }

    // Dispatch search
    req.send(null);
  },

  // Caches individually the rendered results of a search
  // keyword - The keyword's to cache results against
  // resultsXml - The search results as an XML document
  // itemUnixtime - The timestamp of the current item
  _cacheSearchResults : function (keyword, resultsXml) {

    var resCount = 0;

    var res = resultsXml.getElementsByTagName("result");

    // Loop over the results
    for(var i=0;i<res.length;i++) {

      // Cache upto MAX_RESULTS+2 - will exclude current item
      // on rendering, so need 6 to show 5
      // Need 1 extra to know there are "more ..." results
      if (resCount >= this._MAX_RESULTS+2) {

        break;
      }

      var fields = res[i].getElementsByTagName("field");
      var unixtime;
      var title;
      var date;
      var uristub;

      for (var j=0;j<fields.length;j++) {
        var field = fields[j];
        var name = field.getAttribute("name");
        if (name == "unixtime") {
          unixtime = field.firstChild.data;
        } else if (name == "title") {
          title = field.firstChild.data;
        } else if (name == "time") {
          date = field.firstChild.data;
        } else if (name == "uristub") {
          uristub = field.firstChild.data;
        }
      }


      this._cacheResult(keyword,title,uristub,unixtime,date);
      resCount++;
    }
  },

  // Renders and caches completed results pane for a given anchor
  // anchor - The keyword's <a> element
  _displayCachedResults : function (anchor) {

    this._hideResults();
    this._clearResults();

    var keyword = anchor.firstChild.data;
    var anchorId = this._getAnchorId(anchor);

    var results = this._cachedResults[keyword];
    var numResults = 0;
    var paneHTML = new Array();

    if (results) {

      // Loop over the cached results, keyed on their unixtimes
      for (var i in results) {

        // Exclude current item
        if(i != anchor.timestamp) {

          if (numResults < this._MAX_RESULTS) {

            paneHTML[numResults] = results[i];
            numResults++;

          } else {
            break;
          }
        }
      }
    }

    this._resultPane.innerHTML = paneHTML.join('');

    // if no results to render - add "no results" msg
    if (numResults < 1) {
      this._addNoResultsMsg(keyword);
    } else if (numResults >= this._MAX_RESULTS) {

      // Add "more..." link
      this._addMoreLink(keyword);
    }

    this._cachedHtml[anchorId] = this._resultPane.innerHTML;
    this._placeResultsAt(anchor);
  },

  // Redisplays cached HTML for an anchor
  // anchor - The keyword's anchor element
  _displayCachedHtml : function (anchor) {

    this._hideResults();
    this._clearResults();

    var anchorId = this._getAnchorId(anchor);
    
    this._resultPane.innerHTML=this._cachedHtml[anchorId];
    this._placeResultsAt(anchor);
  },

  // Creates the container used to hold results
  _createResultPane : function () {
    this._resultPane = document.createElement("div");
    this._resultPane.id = "cake-results";
    this._resultPane.onmouseout = this._handleMouseOut;
    this._resultPane.onmouseover = this._handleMouseOver;
    this._resultPane.cake = this;

    this._paneAlpha = 99;
    this._updatePaneOpacity();
    
    document.body.appendChild(this._resultPane);
  },

  // Handles mouse event on results pane
  // this -- expected to be result pane
  _handleMouseOver : function () {

    // Prevent pane from being hidden
    this.cake._clearHideTimer();

    // If not already pretty much faded out ...
    if (this.cake._paneAlpha > 25) {
      // Restore from fade
      this.cake._clearFadeInterval();
      this.cake._paneAlpha = 99;
      this.cake._updatePaneOpacity();
    }
  },

  // Handles mouse leaving results pane
  // this -- expected to be result pane
  _handleMouseOut : function (event) {

    // PPK code to ensure a mouse actually leaving pane
    // not just entering a child
    var e = event || window.event;
    var tg = (window.event) ? e.srcElement : e.target;
    if (tg.nodeName != 'DIV') return;
    var reltg = (e.relatedTarget) ? e.relatedTarget : e.toElement;

    // Climb the 'to' element's tree until we reach either the body
    // (actual mouseout), or this element (mousedover a child)
    while (reltg && (reltg != tg) && (reltg.nodeName != 'BODY')) {
      reltg= reltg.parentNode;
    }
    if (reltg == tg) return;

    // 'True' mouseout - start fadeout
    this.cake._beginFade();
  },


  // Clears the timer that would cause search results to be hidden
  _clearHideTimer : function () {

    window.clearTimeout(this._hideTimerId);
  },


  // Begins fading out the results pane
  _beginFade : function () {
    Cake._clearHideTimer();
    Cake._clearFadeInterval();
    Cake._fadeIntervalId = window.setInterval(Cake._tickFade, 25);
  },


  // Incrementally fades the result pane
  _tickFade : function () {
    if (Cake._paneAlpha > 60) {
      Cake._paneAlpha -= 3;
    } else {
      // Accelerate fade
      Cake._paneAlpha -= 7;
    }

    // Clamp at zero
    if (Cake._paneAlpha <= 0) {
      Cake._paneAlpha = 0;
      Cake._resultPane.style.display = 'none';

      // No more fading, please
      Cake._clearFadeInterval();
    }
    Cake._updatePaneOpacity();
  },

  // Stops the fade effect
  _clearFadeInterval : function () {

    window.clearInterval(this._fadeIntervalId);
  },

  // Hides the results pane
  _hideResults : function () {
    this._clearHideTimer();
    this._clearFadeInterval();
    this._resultPane.style.display = 'none';
  },

  // Empties the results pane
  _clearResults : function () {
    this._resultPane.innerHTML="";
  },

  // Positions the results pane
  // element - HTML element to position pane below
  _placeResultsAt : function (element) {

    // Position results
    this._resultPane.style.left = element.offsetLeft+"px";
    this._resultPane.style.top = element.offsetTop+"px";


    // Render the element
    this._resultPane.style.display = 'block';

    // Get the rendered height of the pane
    var paneTop = this._resultPane.offsetTop;
    var paneHeight = this._resultPane.offsetHeight;
    var elementTop = element.offsetTop;
    var elementHeight = element.offsetHeight;
    var windowHeight = 0;
    var windowScroll = 0;
    if (self.innerHeight) {
      windowHeight = self.innerHeight;
      windowScroll = self.pageYOffset;
    } else if (document.documentElement && document.documentElement.clientHeight) {
      windowHeight = document.documentElement.clientHeight;
      windowScroll = document.documentElement.scrollTop;
    } else if (document.body) {
      windowHeight = document.body.clientHeight;
      windowScroll = document.body.scrollTop;
    }


    // See if the pane is below the bottom of the screen
    if ((windowScroll > -1) && (paneTop + paneHeight > windowHeight + windowScroll)) {

      // Frob it upward
      // Need to retoggle the display to avoid a flicker in gecko
      this._resultPane.style.display = 'none';
      this._resultPane.style.top = (elementTop - (elementHeight + paneHeight + 10))+"px";
      this._resultPane.style.display = 'block';
    }

    // Make it visible
    this._paneAlpha = 99;
    this._updatePaneOpacity();
  },

  // Caches search result against a keyword
  // keyword - Keyword to cache against
  // title - Item title
  // uristub - relative link to chump document, sans extension
  // unixtime - timestamp of item
  // date - UTC date of item's chumping
  _cacheResult : function (keyword,title,uristub,unixtime,date) {
    var para = document.createElement("p");

    para.className="cake-para";
    
    var anchor = document.createElement('a');
    anchor.href=this._LINK_PREFIX+uristub+'.html#'+unixtime;

    anchor.appendChild(document.createTextNode(title));

    para.appendChild(anchor);

    var span = document.createElement("span");
    span.className = "cake-result-date";
    span.appendChild(document.createTextNode(" ("+date.substring(0,10)+")"));
    para.appendChild(span);
    if (!this._cachedResults[keyword]) {
      this._cachedResults[keyword] = new Array();
    }
    var tempContainer = document.createElement("div");
    tempContainer.appendChild(para);
    this._cachedResults[keyword][unixtime] = tempContainer.innerHTML; 
  },

  // Adds "no other entries" message to results pane
  // keyword - the keyword searched on
  _addNoResultsMsg : function (keyword) {
    var para = document.createElement("p");
    para.className = "cake-para";

    var span = document.createElement("span");
    span.className = "cake-no-results";
    span.appendChild(document.createTextNode("no other entries for '"+keyword+"'."));
    para.appendChild(span);
    this._resultPane.appendChild(para);
  },

  // Adds link to full search results to pane
  // keyword - The keyword to search on
  _addMoreLink : function (keyword) {
    var para = document.createElement("p");
    para.className = "cake-para";

    var anchor = document.createElement('a');
    anchor.href=this._SEARCH_URL_PREFIX+escape(keyword);

    anchor.appendChild(document.createTextNode("more..."));

    para.appendChild(anchor);
    this._resultPane.appendChild(para);
  },

  // Generates a unique Id for a keyword anchor
  // anchor - The keyword anchor
  _getAnchorId : function (anchor) {
    return anchor.firstChild.data+anchor.timestamp;
  },

  // Sets the pane's opacity for IE and W3C browsers
  _updatePaneOpacity : function () {
    this._resultPane.style.opacity = this._paneAlpha/100;
    this._resultPane.style.filter = "alpha(opacity="+this._paneAlpha+")";
  },

  // Sets/unsets progress styles on a keyword anchor
  // anchor - The keyword anchor
  // setToProgress - True to set, false to unset
  _setProgressStyles : function (anchor, setToProgress) {      

    if (setToProgress) {
        
      anchor.style.cursor = "progress";
      anchor.style.textDecoration = "none";
      anchor.style.opacity = "0.5"
      anchor.style.filter = "alpha(opacity="+50+")";

    } else {

      anchor.style.cursor = "pointer";
      anchor.style.textDecoration = "underline";
      anchor.style.opacity = "0.99";
      anchor.style.filter = null;
    }
  },

  // Jibbering x-platform XmlHttpRequest creator
  // returns XmlHttpRequest instance of null if unsupported
  _getHttpReq : function () {
    var xmlhttp=false;
    try {
      xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
      try {
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (E) {
        xmlhttp = false;
      }
    }
    if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
      xmlhttp = new XMLHttpRequest();
    }
    return xmlhttp;
  }
}

