/*
#ifdef 0
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Tab Previews.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Dão Gottwald <dao@mozilla.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK *****
#endif
 */

/**
 * Tab previews utility, produces thumbnails
 */
var tabPreviews = {
  aspectRatio: 0.5625, // 16:9
  init: function tabPreviews__init() {
    this.width = Math.ceil(screen.availWidth / 5);
    this.height = Math.round(this.width * this.aspectRatio);

    gBrowser.tabContainer.addEventListener("TabSelect", this, false);
    gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
  },
  uninit: function tabPreviews__uninit() {
    gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
    gBrowser.tabContainer.removeEventListener("SSTabRestored", this, false);
    this._selectedTab = null;
  },
  get: function tabPreviews__get(aTab) {
    if (aTab.__thumbnail_lastURI &&
        aTab.__thumbnail_lastURI != aTab.linkedBrowser.currentURI.spec) {
      aTab.__thumbnail = null;
      aTab.__thumbnail_lastURI = null;
    }
    return aTab.__thumbnail || this.capture(aTab, !aTab.hasAttribute("busy"));
  },
  capture: function tabPreviews__capture(aTab, aStore) {
    var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
    thumbnail.mozOpaque = true;
    thumbnail.height = this.height;
    thumbnail.width = this.width;

    var ctx = thumbnail.getContext("2d");
    var win = aTab.linkedBrowser.contentWindow;
    var snippetWidth = win.innerWidth * .6;
    var scale = this.width / snippetWidth;
    ctx.scale(scale, scale);
    ctx.drawWindow(win, win.scrollX, win.scrollY,
                   snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)");

    if (aStore) {
      aTab.__thumbnail = thumbnail;
      aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec;
    }
    return thumbnail;
  },
  handleEvent: function tabPreviews__handleEvent(event) {
    switch (event.type) {
      case "TabSelect":
        if (this._selectedTab &&
            this._selectedTab.parentNode &&
            !this._pendingUpdate) {
          // Generate a thumbnail for the tab that was selected.
          // The timeout keeps the UI snappy and prevents us from generating thumbnails
          // for tabs that will be closed. During that timeout, don't generate other
          // thumbnails in case multiple TabSelect events occur fast in succession.
          this._pendingUpdate = true;
          setTimeout(function (self, aTab) {
            self._pendingUpdate = false;
            if (aTab.parentNode && !aTab.hasAttribute("busy"))
              self.capture(aTab, true);
          }, 2000, this, this._selectedTab);
        }
        this._selectedTab = event.target;
        break;
      case "SSTabRestored":
        this.capture(event.target, true);
        break;
    }
  }
};

/**
 * Ctrl-Tab panel
 */
var ctrlTab = {
  get panel () {
    delete this.panel;
    return this.panel = document.getElementById("ctrlTab-panel");
  },
  get searchField () {
    delete this.searchField;
    return this.searchField = document.getElementById("ctrlTab-search");
  },
  get pagesBar () {
    delete this.pagesBar;
    return this.pagesBar = document.getElementById("ctrlTab-pages");
  },
  get thumbnails () {
    delete this.thumbnails;
    return this.thumbnails = this.panel.getElementsByClassName("ctrlTab-thumbnail");
  },
  get columns () {
    delete this.columns;
    return this.columns = this.thumbnails.length /
                          this.panel.getElementsByClassName("ctrlTab-row").length;
  },
  get closeCharCode () {
    delete this.closeCharCode;
    return this.closeCharCode = document.getElementById("key_close")
                                        .getAttribute("key")
                                        .toLocaleLowerCase().charCodeAt(0);
  },
  get findCharCode () {
    delete this.findCharCode;
    return this.findCharCode = document.getElementById("key_find")
                                       .getAttribute("key")
                                       .toLocaleLowerCase().charCodeAt(0);
  },
  get recentlyUsedLimit () {
    delete this.recentlyUsedLimit;
    return this.recentlyUsedLimit = gPrefService.getIntPref("browser.ctrlTab.recentlyUsedLimit");
  },
  selectedIndex: 0,
  get selected () this.thumbnails.item(this.selectedIndex),
  get isOpen   () this.panel.state == "open" || this.panel.state == "showing",
  get tabCount () this.tabList.length,

  get sticky () this.panel.hasAttribute("sticky"),
  set sticky (val) {
    if (val)
      this.panel.setAttribute("sticky", "true");
    else
      this.panel.removeAttribute("sticky");
    return val;
  },

  get pages () Math.ceil(this.tabCount / this.thumbnails.length),
  get page  () this._page || 0,
  set page  (page) {
    if (page < 0)
      page = this.pages - 1;
    else if (page >= this.pages)
      page = 0;

    if (this.pagesBar.childNodes.length) {
      this.pagesBar.childNodes[this.page].removeAttribute("selected");
      this.pagesBar.childNodes[page].setAttribute("selected", "true");
    }

    this._page = page;
    this.updatePreviews();
    return page;
  },

  get tabList () {
    if (this._tabList)
      return this._tabList;

    var list = Array.slice(gBrowser.mTabs);

    if (this._closing)
      this.detachTab(this._closing, list);

    for (let i = 0; i < gBrowser.tabContainer.selectedIndex; i++)
      list.push(list.shift());

    if (!this._useTabBarOrder && this.recentlyUsedLimit != 0) {
      let recentlyUsedTabs = this._recentlyUsedTabs;
      if (this.recentlyUsedLimit > 0)
        recentlyUsedTabs = this._recentlyUsedTabs.slice(0, this.recentlyUsedLimit);
      for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) {
        list.splice(list.indexOf(recentlyUsedTabs[i]), 1);
        list.unshift(recentlyUsedTabs[i]);
      }
    }

    if (this.searchField.value) {
      list = list.filter(function (tab) {
        let lowerCaseLabel, uri;
        for (let i = 0; i < this.length; i++) {
          if (tab.label.indexOf(this[i]) != -1)
            continue;

          if (!lowerCaseLabel)
            lowerCaseLabel = tab.label.toLocaleLowerCase();
          if (lowerCaseLabel.indexOf(this[i]) != -1)
            continue;

          if (!uri) {
            uri = tab.linkedBrowser.currentURI.spec;
            try {
              uri = decodeURI(uri);
            } catch (e) {}
          }
          if (uri.indexOf(this[i]) != -1)
            continue;

          return false;
        }
        return true;
      }, this.searchField.value.split(/\s+/g));
    }

    return this._tabList = list;
  },

  init: function ctrlTab__init() {
    if (this._recentlyUsedTabs)
      return;
    this._recentlyUsedTabs = [gBrowser.selectedTab];

    var tabContainer = gBrowser.tabContainer;
    tabContainer.addEventListener("TabOpen", this, false);
    tabContainer.addEventListener("TabSelect", this, false);
    tabContainer.addEventListener("TabClose", this, false);

    this._handleCtrlTab =
      gPrefService.getBoolPref("browser.ctrlTab.previews") &&
      (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
       !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
    if (this._handleCtrlTab)
      gBrowser.mTabBox.handleCtrlTab = false;
    document.addEventListener("keypress", this, false);
  },

  uninit: function ctrlTab__uninit() {
    this._recentlyUsedTabs = null;

    var tabContainer = gBrowser.tabContainer;
    tabContainer.removeEventListener("TabOpen", this, false);
    tabContainer.removeEventListener("TabSelect", this, false);
    tabContainer.removeEventListener("TabClose", this, false);

    this.panel.removeEventListener("popuphiding", this, false);
    this.panel.removeEventListener("popupshown", this, false);
    this.panel.removeEventListener("popuphidden", this, false);
    document.removeEventListener("keypress", this, false);
    if (this._handleCtrlTab)
      gBrowser.mTabBox.handleCtrlTab = true;
  },

  search: function ctrlTab__search() {
    if (this.isOpen) {
      this._tabList = null;
      this.buildPagesBar();
      this.goToPage(0, 0);
      this.updatePreviews();
    }
  },

  buildPagesBar: function ctrlTab__buildPagesBar() {
    var pages = this.pages;
    if (pages == 1)
      pages = 0;
    while (this.pagesBar.childNodes.length > pages)
      this.pagesBar.removeChild(this.pagesBar.lastChild);
    while (this.pagesBar.childNodes.length < pages) {
      let pointer = document.createElement("spacer");
      pointer.setAttribute("onclick", "ctrlTab.goToPage(" + this.pagesBar.childNodes.length + ");");
      pointer.setAttribute("class", "ctrlTab-pagePointer");
      this.pagesBar.appendChild(pointer);
    }
  },

  goToPage: function ctrlTab__goToPage(aPage, aIndex) {
    this.page = aPage;
    this.selected.removeAttribute("selected");
    if (aIndex) {
      this.selectedIndex = aIndex;
      while (!this.selected || !this.selected.hasAttribute("valid"))
        this.selectedIndex--;
    } else {
      this.selectedIndex = 0;
    }
    this.updateSelected();
  },

  updatePreviews: function ctrlTab__updatePreviews() {
    var tabs = this.tabList;
    var offset = this.page * this.thumbnails.length;
    for (let i = 0; i < this.thumbnails.length; i++)
      this.updatePreview(this.thumbnails[i], tabs[i + offset]);
  },
  updatePreview: function ctrlTab__updatePreview(aThumbnail, aTab) {
    do {
      if (aThumbnail._tab) {
        if (aThumbnail._tab == aTab)
          break;
        aThumbnail._tab.removeEventListener("DOMAttrModified", this, false);
      }
      aThumbnail._tab = aTab;
      if (aTab)
        aTab.addEventListener("DOMAttrModified", this, false);
    } while (false);

    if (aThumbnail.firstChild)
      aThumbnail.removeChild(aThumbnail.firstChild);
    if (aTab) {
      aThumbnail.appendChild(tabPreviews.get(aTab));
      aThumbnail.setAttribute("valid", "true");
      aThumbnail.setAttribute("label", aTab.label);
      aThumbnail.setAttribute("crop", aTab.crop);
    } else {
      let placeholder = document.createElement("hbox");
      placeholder.height = tabPreviews.height;
      aThumbnail.appendChild(placeholder);
      aThumbnail.removeAttribute("valid");
      aThumbnail.setAttribute("label", "placeholder");
    }
    aThumbnail.width = tabPreviews.width;
  },

  tabAttrModified: function ctrlTab__tabAttrModified(aTab, aAttrName) {
    switch (aAttrName) {
      case "label":
      case "crop":
      case "busy":
        for (let i = this.thumbnails.length - 1; i >= 0; i--) {
          if (this.thumbnails[i]._tab == aTab) {
            this.updatePreview(this.thumbnails[i], aTab);
            break;
          }
        }
        break;
    }
  },

  advanceSelected: function ctrlTab__advanceSelected() {
    this.selected.removeAttribute("selected");

    this.selectedIndex += this.invertDirection ? -1 : 1;
    if (this.selectedIndex < 0) {
      this.page--;
      this.selectedIndex = this.thumbnails.length - 1;
      while (!this.selected.hasAttribute("valid"))
        this.selectedIndex--;
    } else if (this.selectedIndex >= this.thumbnails.length || !this.selected.hasAttribute("valid")) {
      this.page++;
      this.selectedIndex = 0;
    }
    this.updateSelected();
  },

  updateSelected: function ctrlTab__updateSelected() {
    if (this.tabCount)
      this.selected.setAttribute("selected", "true");
  },

  selectThumbnail: function ctrlTab__selectThumbnail(aThumbnail) {
    if (this.tabCount) {
      this._tabToSelect = (aThumbnail || this.selected)._tab;
      this.panel.hidePopup();
    }
  },

  attachTab: function ctrlTab__attachTab(aTab, aPos) {
    if (aPos == 0)
      this._recentlyUsedTabs.unshift(aTab);
    else if (aPos)
      this._recentlyUsedTabs.splice(aPos, 0, aTab);
    else
      this._recentlyUsedTabs.push(aTab);
  },
  detachTab: function ctrlTab__detachTab(aTab, aTabs) {
    var tabs = aTabs || this._recentlyUsedTabs;
    var i = tabs.indexOf(aTab);
    if (i >= 0)
      tabs.splice(i, 1);
  },

  open: function ctrlTab__open(aSticky) {
    if (this.isOpen && this.sticky) {
      this.panel.hidePopup();
      return;
    }
    this.sticky = !!aSticky;

    this._deferOnTabSelect = [];
    if (this.invertDirection)
      this._useTabBarOrder = true;

    this._tabBarHandlesCtrlPageUpDown = gBrowser.mTabBox.handleCtrlPageUpDown;
    gBrowser.mTabBox.handleCtrlPageUpDown = false;

    document.addEventListener("keyup", this, true);
    document.addEventListener("keydown", this, true);
    this.panel.addEventListener("popupshown", this, false);
    this.panel.addEventListener("popuphiding", this, false);
    this.panel.addEventListener("popuphidden", this, false);
    this._prevFocus = document.commandDispatcher.focusedElement ||
                      document.commandDispatcher.focusedWindow;
    this.panel.hidden = false;
    this.panel.width = screen.availWidth * .85;
    this.panel.popupBoxObject.setConsumeRollupEvent(Ci.nsIPopupBoxObject.ROLLUP_CONSUME);
    this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
                                 screen.availTop + screen.availHeight * .12,
                                 false);
    this.buildPagesBar();
    this.selectedIndex = 0;
    this.page = 0;
    this.advanceSelected();
  },

  onKeyPress: function ctrlTab__onKeyPress(event) {
    var isOpen = this.isOpen;

    if (isOpen &&
        event.target == this.searchField &&
        event.keyCode != event.DOM_VK_ESCAPE)
      return;

    if (isOpen) {
      event.preventDefault();
      event.stopPropagation();
    }

    switch (event.keyCode) {
      case event.DOM_VK_TAB:
        if ((event.ctrlKey || this.sticky) && !event.altKey && !event.metaKey) {
          this.invertDirection = event.shiftKey;
          if (isOpen) {
            this.advanceSelected();
          } else if (this._handleCtrlTab) {
            event.preventDefault();
            event.stopPropagation();
            if (gBrowser.mTabs.length > 2) {
              this.open();
            } else if (gBrowser.mTabs.length == 2) {
              gBrowser.selectedTab = gBrowser.selectedTab.nextSibling ||
                                     gBrowser.selectedTab.previousSibling;
            }
          }
        }
        break;
      case event.DOM_VK_UP:
        if (isOpen) {
          let index = this.selectedIndex - this.columns;
          if (index < 0) {
            this.goToPage(this.page - 1, this.thumbnails.length + index);
          } else {
            this.selected.removeAttribute("selected");
            this.selectedIndex = index;
            this.updateSelected();
          }
        }
        break;
      case event.DOM_VK_DOWN:
        if (isOpen) {
          let index = this.selectedIndex + this.columns;
          if (index >= this.thumbnails.length || !this.thumbnails[index].hasAttribute("valid")) {
            this.goToPage(this.page + 1, this.selectedIndex % this.columns);
          } else {
            this.selected.removeAttribute("selected");
            this.selectedIndex = index;
            while (!this.selected.hasAttribute("valid"))
              this.selectedIndex--;
            this.updateSelected();
          }
        }
        break;
      case event.DOM_VK_LEFT:
        if (isOpen) {
          this.invertDirection = true;
          this.advanceSelected();
        }
        break;
      case event.DOM_VK_RIGHT:
        if (isOpen) {
          this.invertDirection = false;
          this.advanceSelected();
        }
        break;
      case event.DOM_VK_HOME:
        if (isOpen)
          this.goToPage(0);
        break;
      case event.DOM_VK_END:
        if (isOpen)
          this.goToPage(this.pages - 1, this.thumbnails.length - 1);
        break;
      case event.DOM_VK_PAGE_UP:
        if (isOpen)
          this.goToPage(this.page - 1);
        break;
      case event.DOM_VK_PAGE_DOWN:
        if (isOpen)
          this.goToPage(this.page + 1);
        break;
      case event.DOM_VK_RETURN:
        if (isOpen && this.sticky)
          this.selectThumbnail();
        break;
      case event.DOM_VK_ESCAPE:
        if (isOpen)
          this.panel.hidePopup();
        break;
      default:
        if (isOpen && event.ctrlKey) {
          switch (event.charCode) {
            case this.closeCharCode:
              gBrowser.removeTab(this.selected._tab);
              break;
            case this.findCharCode:
              this.searchField.focus();
              break;
          }
        }
    }
  },
  onPopupHiding: function ctrlTab__onPopupHiding() {
    gBrowser.mTabBox.handleCtrlPageUpDown = this._tabBarHandlesCtrlPageUpDown;
    document.removeEventListener("keyup", this, true);
    document.removeEventListener("keydown", this, true);

    this.selected.removeAttribute("selected");
    if (this.pagesBar.childNodes.length)
      this.pagesBar.childNodes[this.page].removeAttribute("selected");

    Array.forEach(this.thumbnails, function (thumbnail) {
      this.updatePreview(thumbnail, null);
    }, this);

    this.searchField.value = "";
    this.invertDirection = false;
    this.sticky = false;
    this._useTabBarOrder = false;
    this._page = null;
    this._tabList = null;

    this._deferOnTabSelect.forEach(this.onTabSelect, this);
    this._deferOnTabSelect = null;

    this._prevFocus.focus();
    this._prevFocus = null;

    if (this._tabToSelect) {
      gBrowser.selectedTab = this._tabToSelect;
      this._tabToSelect = null;
    }
  },
  onTabSelect: function ctrlTab__onTabSelect(aTab) {
    if (aTab.parentNode) {
      this.detachTab(aTab);
      this.attachTab(aTab, 0);
    }
  },

  removeClosingTabFromUI: function ctrlTab__removeClosingTabFromUI(aTab) {
    this._closing = aTab;
    this._tabList = null;
    if (this.tabCount == 1) {
      this.panel.hidePopup();
    } else {
      this.buildPagesBar();
      this.updatePreviews();
      if (!this.selected.hasAttribute("valid"))
        this.advanceSelected();
      else
        this.updateSelected();
    }
    this._closing = null;
  },

  handleEvent: function ctrlTab__handleEvent(event) {
    switch (event.type) {
      case "DOMAttrModified":
        this.tabAttrModified(event.target, event.attrName);
        break;
      case "TabSelect":
        if (this.isOpen)
          // don't change the tab order while the panel is open
          this._deferOnTabSelect.push(event.target);
        else
          this.onTabSelect(event.target);
        break;
      case "TabOpen":
        this.attachTab(event.target, 1);
        break;
      case "TabClose":
        this.detachTab(event.target);
        if (this.isOpen)
          this.removeClosingTabFromUI(event.target);
        break;
      case "keypress":
        this.onKeyPress(event);
        break;
      case "keydown":
      case "keyup":
        if (event.target == this.searchField) {
          if (event.keyCode == event.DOM_VK_RETURN) {
            // If there's a pending search, kick it off now.
            if (this.searchField._timer)
              this.search();
            this.selectThumbnail();
          }
        } else {
          // Manually consume the events, as the panel is open but doesn't
          // necessarily have focus.
          event.stopPropagation();
          event.preventDefault();
        }

        if (!this.sticky &&
            event.type == "keyup" &&
            event.keyCode == event.DOM_VK_CONTROL)
          this.selectThumbnail();
        break;
      case "popupshown":
        if (this.sticky && event.target == this.panel)
          this.searchField.focus();
        break;
      case "popuphiding":
        if (event.target == this.panel)
          this.onPopupHiding();
        break;
      case "popuphidden":
        if (event.target == this.panel) {
          // Destroy the widget in order to prevent outdated content
          // when re-opening the panel.
          this.panel.hidden = true;
        }
        break;
    }
  }
};
