if (typeof W4 == 'undefined' || !W4) {
    var W4 = {};
}

W4.ui = W4.ui || {};

(function() {

  var Event = YAHOO.util.Event;
  var Dom = YAHOO.util.Dom;
  var Element = YAHOO.util.Element;

  // PageScroller --------------------------------------------------------------

  W4.ui.PageScroller = function(container, pageIDs, baseURL) {
    if (arguments.length) {
      this.init(container, pageIDs, baseURL);
    }
  };

  W4.ui.PageScroller.prototype = {

    init: function(container, pageIDs, baseURL) {
      _initPageScroller.apply(this, arguments);
    },

    /**
     * Returns the page currently scrolled into view. This is defined as the 
     * page with its top edge closest to but not below the center of the 
     * viewport.
     *
     * @method getCurrentPage
     * @return {W4.ui.ScannedPage} the page currently scrolled into view
     */
    getCurrentPage: function() {
      var center = (Dom.getY(this._container) 
                    + (this._container.offsetHeight / 2));
      var min = PAGE_HEIGHT;
      var current = null;
      for (var page_id in this._pages) {
        var y = Dom.getY(page_id);
        if (y > center) {
          continue;
        }
        if (center - y < min) {
          min = center - y;
          current = this._pages[page_id];
        }
      }
      return current;
    },

    /**
     * Scrolls the page container so that the top of the specified page is at 
     * the top of the viewport.
     *
     * @method getCurrentPage
     * @param {String} page_id  ID of the page to scroll to
     */
    scrollToPage: function(page_id) {
      this._container.scrollTop = this._pages[page_id]._page.offsetTop;
    },

    /**
     * Given an array of locators, highlights those page locations and scrolls
     * to the first highlighted location. Each member of the array is a pair 
     * (array of length 2) consisting of a page ID and a locator object 
     * specifying a word sequence location within that page (see getSelection 
     * for a detailed explanation of locator objects).
     *
     * @method highlight
     * @param {Array} locators  an array of page ID + locator pairs
     */
    highlight: function(locators) {
      this.clearHighlights();
      var minY = 1000000; 
      for (var i = 0; i < locators.length; i++) {
        var page_id = locators[i][0];
        var locator = locators[i][1];
        var y = this._pages[page_id].highlight(locator);
        if (y < minY) { minY = y; }
      }
      this._container.scrollTop = minY;
    },

    /**
     * Clears all highlighted page locations.
     *
     * @method clearHighlights
     */
    clearHighlights: function() {
      for (var page_id in this._pages) {
        this._pages[page_id].clearHighlights();
      }
    }
  };

  YAHOO.lang.augment(W4.ui.PageScroller, YAHOO.util.EventProvider); 

  // ScannedPage ---------------------------------------------------------------

  /**
   * Uses OCR word bounding-box data to simulate selectable text using a 
   * scanned page image.
   *
   * @namespace W4.ui
   * @class ScannedPage
   * @uses YAHOO.util.EventProvider
   * @param {String} page_id  ID of the element that will hold the page image
   * @param {String} ocr_data  an array of OCR bounding-box data
   */
  W4.ui.ScannedPage = function(page_id, ocr_data) {
    if (arguments.length) {
      this.init(page_id, ocr_data);
    }
  };

  W4.ui.ScannedPage.prototype = {

    init: function(page_id, ocr_data) {
      _initScannedPage.apply(this, arguments);
    },

    /**
     * Returns the OCR'd text of the page as a string.
     *
     * @method getText
     * @return {String} OCR'd text of the page
     */
    getText: function() {
      if (this._text == null) {
        this._text = '';
        for (var i = 0; i < this._lines.length; i++) {
          for (var j = 0; j < this._lines[i].words.length; j++) {
            if (i > 0 || j > 0) { this._text += ' '; }
            this._text += this._lines[i].words[j].chars;
          }
        }
      }
      return this._text;
    },

    /**
     * Returns an object with with information about the first location of the
     * specified text, or null if the text cannot be found. See getSelection 
     * for an explanation of the object properties.
     *
     * @method find
     * @param {String} text  The text to find
     * @param {Number} start_line  The index of the line to start searching 
     *                             from (default 0)
     * @param {Number} start_word  The index of the word to start searching 
     *                             from (default 0)
     * @return {Object} information about location of the text, or null
     */
    find: function(text, start_line, start_word) {
      if (text == null || text.length == 0) { return null; }
      if (! start_line) { start_line = 0; }
      if (! start_word) { start_word = 0; }
      var find_chars = text.split('');
      var result = { 
        text: '', word_count: 0, locator: {}, last_line: 0, last_word: 0 };
      var matched_index = 0;
      for (var i = start_line; i < this._lines.length; i++) {
        for (var j = start_word; j < this._lines[i].words.length; j++) {
          if (find_chars[matched_index] == ' ') { matched_index++; }
          var ocr_chars = this._lines[i].words[j].chars.split('');
          for (var k = 0; k < ocr_chars.length; k++) {
            if (ocr_chars[k] == find_chars[matched_index]) {
              matched_index++;
              if (matched_index == find_chars.length) {
                break;
              }
            } else {
              matched_index = 0;
              break;
            }
          }
          if (matched_index > 0) { 
            // append to result
            if (i > 0 || j > 0) { result.text += ' '; }
            result.text += this._lines[i].words[j].chars;
            result.word_count += 1;
            if (! YAHOO.lang.hasOwnProperty(result.locator, i)) {
              result.locator[i] = [];
            }
            result.locator[i].push(j);
            result.last_line = i;
            result.last_word = j;
          } else { 
            // clear result
            result.text = ''; 
            result.word_count = 0; 
            result.locator = {};
            result.last_line = result.last_word = 0;
          }
          if (matched_index == find_chars.length) {
            return result;
          }
        }
        start_word = 0;
      }
      return null;
    },

    /**
     * Returns an object with information about the currently selected text.
     * The object has three properties: 'text' is a string with the selected 
     * text, 'word_count' is the number of selected words, and 'locator' is a 
     * map specifying the positions of the selected words. For each key-value 
     * pair in the map, the key is the line index, and the value is an array 
     * of word indexes in that line, e.g. { 2: [ 3, 4 ] } would be the fourth 
     * and fifth words in in the third line.
     *
     * @method getSelection
     * @return {Object} information about the currently selected text
     */
    getSelection: function() {
      this._selection.text = '';
      if (this._selection.word_count > 0) {
        var line_indexes = [];
        for (var line_index in this._selection.locator) {
          if (YAHOO.lang.hasOwnProperty(this._selection.locator, line_index)) {
            line_indexes.push(line_index);
          }
        } 
        line_indexes.sort(function(a, b) { return a - b; });
        for (var i = 0; i < line_indexes.length; i++) {
          var line_idx = line_indexes[i];
          for (var j = 0; j < this._selection.locator[line_idx].length; j++) {
            var word_idx = this._selection.locator[line_idx][j];
            if (i > 0 || j > 0) { this._selection.text += ' '; }
            this._selection.text += this._lines[line_idx].words[word_idx].chars;
          }
        }
      }
      return this._selection;
    },

    /**
     * Clears any selected text in the page.
     *
     * @method clearSelection
     */
    clearSelection: function() {
      this._selection.text = '';
      this._selection.word_count = 0;
      this._selection.locator = {};
      this._startLine = this._endLine = -1;
      this.clearHighlights();
    },

    /**
     * Given a locator object, highlights that page location. 
     * See getSelection for a detailed explanation of locator objects.
     *
     * @method highlight
     * @param {Array} locator  a word sequence locator object
     */
    highlight: function(locator) {
      var minY = PAGE_HEIGHT;
      for (var line_index in locator) {
        var word_indexes = locator[line_index];
        for (var i = 0; i < word_indexes.length; i++) {
          var line = this._lines[Number(line_index)];
          var word = line.words[word_indexes[i]];
          _highlightWord.call(this, line, word);
          if (line.y < minY) { minY = line.y; }
        }
      }
      return this._page.offsetTop + minY;
    },

    /**
     * Clears all highlighted page locations.
     *
     * @method clearHighlights
     */
    clearHighlights: function() {
      Dom.getElementsByClassName('highlight', 'div', this._page, function(el) {
        Dom.setStyle(el, 'display', 'none');
      });
    },

    /**
     * Given a Y position relative to the page image, returns the index of the 
     * closest line of text.
     *
     * @method getLineIndex
     * @param {Number} y  Y position relative to the page image
     * @return {Number} index of the closet line of text
     */
    getLineIndex: function(y) {
      if (y < this._top) { return 0; } 
      if (y > this._bottom) { return this._lines.length - 1; }
      var index = -1; 
      var minH = 100000;
      for (var i = 0; i < this._lines.length; i++) {
        if (this._lines[i].y > y) {
          return index;
        }
        if ((y >= this._lines[i].y) && 
            (y <= this._lines[i].y + this._lines[i].h)) {
          if (this._lines[i].h < minH) {
            index = i;
            minH = this._lines[i].h;
          }
        }
      }
      return index;
    },

    /**
     * Given a mouse event, returns the X position relative to the page image
     * where that event occurred.
     *
     * @method getEventX
     * @param {Event} e  a mouse event
     * @return {Number}  X position relative to the page image
     */
    getEventX: function(e) {
      return (Event.getPageX(e) - Dom.getX(this._page));

    },

    /**
     * Given a mouse event, returns the Y position relative to the page image
     * where that event occurred.
     *
     * @method getEventY
     * @param {Event} e  a mouse event
     * @return {Number}  Y position relative to the page image
     */
    getEventY: function(e) {
      return (Event.getPageY(e) - Dom.getY(this._page));
    }

  };

  YAHOO.lang.augment(W4.ui.ScannedPage, YAHOO.util.EventProvider); 

  // Private methods -----------------------------------------------------------

  var PAGE_HEIGHT = 1200;
  var GROUP_SIZE = 2;

  var _initPageScroller = function(container, pageIDs, baseURL) {
    if (! container.id) { container.id = 'w4-ui-scroll-container'; }
    this._container = container;
    this._baseURL = baseURL;
    this._pageGroups = [];
    this._pageGroupTops = [];
    this._nextGroupIndex = 0;
    this._pages = {};
    this._updating = false;
    _registerPageImages.call(this, pageIDs);
    _updateView.call(this);
    Event.on(this._container, 'mousedown', _delegateMousedown, this, true);
    Event.on(this._container, 'scroll', _updateView, this, true);
    this.createEvent('select');
  };

  var _registerPageImages = function(pageIDs) {
    var pageGroup = null;
    for (var i = 0; i < pageIDs.length; i++) {
      if (i % GROUP_SIZE == 0) {
        pageGroup = new YAHOO.util.ImageLoader.group();
        pageGroup.name = 'pageGroup-' + this._pageGroups.length;
        this._pageGroups.push(pageGroup);
        this._pageGroupTops.push(Dom.getY('page-' + pageIDs[i]));
      }
      pageGroup.registerPngBgImage(
        'page-' + pageIDs[i], 
        this._baseURL + 'IMAGES_PNG/' + pageIDs[i] + '.png');
    }
  };

  var _addPage = function(page) {
    var self = this;
    page.subscribe('select', function(e) { self.fireEvent('select', e); });
    this._pages[page.id] = page;
  };

  var _updateView = function() {
    if (this._updating) { return; }
    this._updating = true;
    var self = this;
    var pages = this._pages;
    var bottomEdge = this._container.scrollTop + this._container.offsetHeight;
    while ((this._pageGroupTops[this._nextGroupIndex] - bottomEdge) 
           < (PAGE_HEIGHT * GROUP_SIZE)) {
      if (this._nextGroupIndex == this._pageGroups.length) {
        Event.removeListener(this._container, 'scroll', _updateView);
        break;
      }
      var pageGroup = this._pageGroups[this._nextGroupIndex++];
      pageGroup.fetch();
      for (var page_id in pageGroup._imgObjs) {
        if (YAHOO.lang.hasOwnProperty(pageGroup._imgObjs, page_id)) {
          var callback = {
            onSuccess: function(o) {
              var page_id = o.data;
              _addPage.call(
                self, new W4.ui.ScannedPage(page_id, ocr_data[page_id]));
            },
            onFailure: function(o) {
              alert('Failed to load OCR data for ' + o.data);
            },
            data: page_id
          };
          YAHOO.util.Get.script(
            this._baseURL + 'OCR_JSON/' + page_id.split('-')[1] + '.json', 
            callback);
        }
      }
    }
    this._updating = false;
  };

  var _delegateMousedown = function(e) {
    var target = Event.getTarget(e); 
    while (target.id != this._container.id) {
      if (target.className == 'page') {
        _beginSelect.call(this._pages[target.id], e);
        break;
      }
      target = target.parentNode;
    }
  };

  var _initScannedPage = function(page_id, ocr_data) {
    this.id = page_id;
    this._page = document.getElementById(page_id);
    this._lines = ocr_data;
    this._text = null;
    this._selection = { text: '', word_count: 0, locator: {}, 
                        resource: location.href + '#' + page_id };
    this._startLine = -1;
    this._endLine = -1;
    this._startX = 0;
    this._startY = 0;
    this._top = this._lines[0].y;
    this._bottom = (this._lines[this._lines.length - 1].y + 
                    this._lines[this._lines.length - 1].h);
    this.createEvent('select');
  };

  var _beginSelect = function(e) {
    this.clearSelection();
    this._startX = this.getEventX(e);
    this._startY = this.getEventY(e);
    if (this._startY < this._top || this._startY > this._bottom) { return; }
    this._startLine = this._endLine = this.getLineIndex(this._startY);
    if (this._startLine >= 0) {
      Event.on(document, 'mousemove', _updateSelect, this, true);
      Event.on(document, 'mouseup', _finishSelect, this, true);
    }
  };

  var _lineBottom = function(lines, index) {
    if (index + 1 < lines.length) {
      return lines[index + 1].y;
    }
    return lines[index].y + lines[index].h;
  };

  var _updateSelect = function(e) {
    _autoscroll.call(this, e);
    var startX = this._startX; 
    var x = this.getEventX(e);
    var y = this.getEventY(e);
    if (y >= this._startY) { 
      // selecting downwards
      while (this._endLine < this._startLine) {
        _unhighlight.call(this, this._endLine++);
      }
      if (this._startLine < this._endLine) {
        startX = this._lines[this._endLine].x;
      }
      while (y > _lineBottom(this._lines, this._endLine)) {
        _highlight.call(
          this, this._endLine, startX, (this._lines[this._endLine].x + 
                                        this._lines[this._endLine].w));
        if (this._endLine == this._lines.length - 1) { break; } 
        this._endLine++;
        startX = this._lines[this._endLine].x;
      }
      while (y < this._lines[this._endLine].y) {
        _unhighlight.call(this, this._endLine--);
        startX = (this._startLine == this._endLine ? 
                  this._startX : this._lines[this._endLine].x);
      }
      _highlight.call(this, this._endLine, startX, x);
    } else { 
      // selecting upwards
      while (this._endLine > this._startLine) {
        _unhighlight.call(this, this._endLine--);
      }
      if (this._startLine > this._endLine) {
        startX = (this._lines[this._endLine].x + this._lines[this._endLine].w);
      }
      while (y < this._lines[this._endLine].y) {
        _highlight.call(
          this, this._endLine, startX, this._lines[this._endLine].x);
        if (this._endLine == 0) { break; }
        this._endLine--;
        startX = (this._lines[this._endLine].x + this._lines[this._endLine].w);
      }
      while (y > _lineBottom(this._lines, this._endLine)) {
        _unhighlight.call(this, this._endLine++);
        startX = (this._startLine == this._endLine ? 
                  this._startX : (this._lines[this._endLine].x + 
                                  this._lines[this._endLine].w));
      }
      _highlight.call(this, this._endLine, startX, x);
    }
  };

  var _finishSelect = function(e) {
    Event.removeListener(document, 'mousemove', _updateSelect);
    Event.removeListener(document, 'mouseup', _finishSelect);
    if (this._selection.word_count > 0) {
      this.fireEvent('select', { target: this, event: e }); 
    }
  };

  var PADX = 4;
  var PADY = 2;

  var _highlight = function(line_index, startX, endX) {
    if (endX < startX) { var tmp = startX; startX = endX; endX = tmp; }
    var line = this._lines[line_index];
    if (startX < line.x) { startX = line.x; }
    if (endX > line.x + line.w) { endX = line.x + line.w; }
    if (YAHOO.lang.hasOwnProperty(this._selection.locator, line_index)) {
      this._selection.word_count -= this._selection.locator[line_index].length;
    }
    this._selection.locator[line_index] = [];
    for (var word_index = 0; word_index < line.words.length; word_index++) {
      var word = line.words[word_index];
      if ((word.x <= startX && word.x + word.w >= endX) ||
          (word.x >= startX && word.x <= endX) ||
          (word.x + word.w >= startX && word.x + word.w <= endX)) {

        _highlightWord.call(this, line, word);
        this._selection.locator[line_index].push(word_index);
        this._selection.word_count++;

      } else {
        if (word.highlight) {
          word.highlight.setStyle('display', 'none');
        }
      }
    }
    line.highlight.setStyle('display', 'block');
  };

  var _highlightWord = function(line, word) {
    if (! line.highlight) {
      line.highlight = new Element(document.createElement('div'));
      line.highlight.appendTo(this._page);
    } else {
      line.highlight.setStyle('display', 'block');
    }
    if (! word.highlight) {
      word.highlight = new Element(document.createElement('div'));
      word.highlight.set('className', 'highlight');
      word.highlight.setStyle(
        'left', (this._page.offsetLeft + word.x - PADX) + 'px');
      word.highlight.setStyle(
        'top', (this._page.offsetTop + word.y - PADY) + 'px');
      word.highlight.setStyle('width', (word.w  + 2 * PADX) + 'px');
      word.highlight.setStyle('height', (word.h + 2 * PADY) + 'px');
      word.highlight.appendTo(line.highlight);
    } else {
      word.highlight.setStyle('display', 'block');
    }
  };

  var _unhighlight = function(line_index) {
    if (YAHOO.lang.hasOwnProperty(this._selection.locator, line_index)) {
      this._selection.word_count -= this._selection.locator[line_index].length;
    }
    this._selection.locator[line_index] = [];
    var line = this._lines[line_index];
    if (line.highlight) {
      line.highlight.setStyle('display', 'none');
    }
  };

  var _autoscroll = function(e) {
    var container = this._page.offsetParent;

    var position = Dom.getXY(container);
    var x = Event.getPageX(e) - position[0];
    var y = Event.getPageY(e) - position[1];

    var toBottom = container.offsetHeight - y;
    var toRight = container.offsetWidth - x;

    var threshold = 40;
    var scrollAmount = (document.all) ? 80 : 20;
    
    if (toBottom < threshold) { 
      container.scrollTop += scrollAmount;
    }
    if (y < threshold) { 
      container.scrollTop -= scrollAmount;
    }
    if (toRight < threshold) { 
      container.scrollLeft += scrollAmount;
    }
    if (x < threshold) { 
      container.scrollLeft -= scrollAmount;
    }
  };


})();
