/*
 * This file is part of SpyBlock <https://www.gnu.org/software/gnuzilla/>,
 * Copyright (C) 2006-2015 Eyeo GmbH (Adblock Plus) 
 * Copyright (C) 2016 Ruben Rodriguez <ruben@gnu.org>
 *
 * SpyBlock is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * SpyBlock is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with SpyBlock.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @fileOverview Code responsible for showing and hiding object tabs.
 */

/**
 * Class responsible for showing and hiding object tabs.
 * @class
 */
var objTabs =
{
  /**
   * Number of milliseconds to wait until hiding tab after the mouse moves away.
   * @type Integer
   */
  HIDE_DELAY: 1000,

  /**
   * Flag used to trigger object tabs initialization first time object tabs are
   * used.
   * @type Boolean
   */
  initialized: false,

  /**
   * Will be set to true while initialization is in progress.
   * @type Boolean
   */
  initializing: false,

  /**
   * Parameters for _showTab, to be called once initialization is complete.
   */
  delayedShowParams: null,

  /**
   * Randomly generated class to be used for visible object tabs on top of object.
   * @type String
   */
  objTabClassVisibleTop: null,

  /**
   * Randomly generated class to be used for visible object tabs at the bottom of the object.
   * @type String
   */
  objTabClassVisibleBottom: null,

  /**
   * Randomly generated class to be used for invisible object tabs.
   * @type String
   */
  objTabClassHidden: null,

  /**
   * Document element the object tab is currently being displayed for.
   * @type Element
   */
  currentElement: null,

  /**
   * Windows that the window event handler is currently registered for.
   * @type Array of Window
   */
  windowListeners: null,

  /**
   * Panel element currently used as object tab.
   * @type Element
   */
  objtabElement: null,

  /**
   * Time of previous position update.
   * @type Integer
   */
  prevPositionUpdate: 0,

  /**
   * Timer used to update position of the object tab.
   * @type nsITimer
   */
  positionTimer: null,

  /**
   * Timer used to delay hiding of the object tab.
   * @type nsITimer
   */
  hideTimer: null,

  /**
   * Used when hideTimer is running, time when the tab should be hidden.
   * @type Integer
   */
  hideTargetTime: 0,

  /**
   * Initializes object tabs (generates random classes and registers stylesheet).
   */
  _initCSS: function()
  {
    function processCSSData(request)
    {
      if (onShutdown.done)
        return;

      let data = request.responseText;

      let rnd = [];
      let offset = "a".charCodeAt(0);
      for (let i = 0; i < 60; i++)
        rnd.push(offset + Math.random() * 26);

      this.objTabClassVisibleTop = String.fromCharCode.apply(String, rnd.slice(0, 20));
      this.objTabClassVisibleBottom = String.fromCharCode.apply(String, rnd.slice(20, 40));
      this.objTabClassHidden = String.fromCharCode.apply(String, rnd.slice(40, 60));

      let {Utils} = require("utils");
      let url = Utils.makeURI("data:text/css," + encodeURIComponent(data.replace(/%%CLASSVISIBLETOP%%/g, this.objTabClassVisibleTop)
                                                                        .replace(/%%CLASSVISIBLEBOTTOM%%/g, this.objTabClassVisibleBottom)
                                                                        .replace(/%%CLASSHIDDEN%%/g, this.objTabClassHidden)));
      Utils.styleService.loadAndRegisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET);
      onShutdown.add(function()
      {
        Utils.styleService.unregisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET);
      });

      this.initializing = false;
      this.initialized = true;

      if (this.delayedShowParams)
        this._showTab.apply(this, this.delayedShowParams);
    }

    this.delayedShowParams = arguments;

    if (!this.initializing)
    {
      this.initializing = true;

      // Load CSS asynchronously
      try {
        let request = new XMLHttpRequest();
        request.mozBackgroundRequest = true;
        request.open("GET", "chrome://adblockplus/content/objtabs.css");
        request.overrideMimeType("text/plain");

        request.addEventListener("load", processCSSData.bind(this, request), false);
        request.send(null);
      }
      catch (e)
      {
        Cu.reportError(e);
        this.initializing = false;
      }
    }
  },

  /**
   * Called to show object tab for an element.
   */
  showTabFor: function(/**Element*/ element)
  {
    // Object tabs aren't usable in Fennec
    let {application} = require("info");
    if (application == "fennec" || application == "fennec2" || application == "icecatmobile")
      return;

    let {Prefs} = require("prefs");
    if (!Prefs.frameobjects)
      return;

    if (this.hideTimer)
    {
      this.hideTimer.cancel();
      this.hideTimer = null;
    }

    if (this.objtabElement)
      this.objtabElement.style.setProperty("opacity", "1", "important");

    if (this.currentElement != element)
    {
      this._hideTab();

      let {Policy} = require("contentPolicy");
      let {RequestNotifier} = require("requestNotifier");
      let data = RequestNotifier.getDataForNode(element, true, Policy.type.OBJECT);
      if (data)
      {
        if (this.initialized)
          this._showTab(element, data[1]);
        else
          this._initCSS(element, data[1]);
      }
    }
  },

  /**
   * Called to hide object tab for an element (actual hiding happens delayed).
   */
  hideTabFor: function(/**Element*/ element)
  {
    if (element != this.currentElement || this.hideTimer)
      return;

    this.hideTargetTime = Date.now() + this.HIDE_DELAY;
    this.hideTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this.hideTimer.init(this, 40, Ci.nsITimer.TYPE_REPEATING_SLACK);
  },

  /**
   * Makes the tab element visible.
   * @param {Element} element
   * @param {RequestEntry} data
   */
  _showTab: function(element, data)
  {
    let {UI} = require("ui");
    if (!UI.overlay)
      return;

    let doc = element.ownerDocument.defaultView.top.document;

    this.objtabElement = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
    this.objtabElement.textContent = UI.overlay.attributes.objtabtext;
    this.objtabElement.setAttribute("title", UI.overlay.attributes.objtabtooltip);
    this.objtabElement.setAttribute("href", data.location);
    this.objtabElement.setAttribute("class", this.objTabClassHidden);
    this.objtabElement.style.setProperty("opacity", "1", "important");
    this.objtabElement.nodeData = data;

    this.currentElement = element;

    // Register paint listeners for the relevant windows
    this.windowListeners = [];
    let wnd = element.ownerDocument.defaultView;
    while (wnd)
    {
      wnd.addEventListener("MozAfterPaint", objectWindowEventHandler, false);
      this.windowListeners.push(wnd);
      wnd = (wnd.parent != wnd ? wnd.parent : null);
    }

    // Register mouse listeners on the object tab
    this.objtabElement.addEventListener("mouseover", objectTabEventHander, false);
    this.objtabElement.addEventListener("mouseout", objectTabEventHander, false);
    this.objtabElement.addEventListener("click", objectTabEventHander, true);

    // Insert the tab into the document and adjust its position
    doc.documentElement.appendChild(this.objtabElement);
    if (!this.positionTimer)
    {
      this.positionTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
      this.positionTimer.init(this, 200, Ci.nsITimer.TYPE_REPEATING_SLACK);
    }
    this._positionTab();
  },

  /**
   * Hides the tab element.
   */
  _hideTab: function()
  {
    this.delayedShowParams = null;

    if (this.objtabElement)
    {
      // Prevent recursive calls via popuphidden handler
      let objtab = this.objtabElement;
      this.objtabElement = null;
      this.currentElement = null;

      if (this.hideTimer)
      {
        this.hideTimer.cancel();
        this.hideTimer = null;
      }

      if (this.positionTimer)
      {
        this.positionTimer.cancel();
        this.positionTimer = null;
      }

      try {
        objtab.parentNode.removeChild(objtab);
      } catch (e) {}
      objtab.removeEventListener("mouseover", objectTabEventHander, false);
      objtab.removeEventListener("mouseout", objectTabEventHander, false);
      objtab.nodeData = null;

      for (let wnd of this.windowListeners)
        wnd.removeEventListener("MozAfterPaint", objectWindowEventHandler, false);
      this.windowListeners = null;
    }
  },

  /**
   * Updates position of the tab element.
   */
  _positionTab: function()
  {
    // Test whether element is still in document
    let elementDoc = null;
    try
    {
      elementDoc = this.currentElement.ownerDocument;
    } catch (e) {}  // Ignore "can't access dead object" error
    if (!elementDoc || !this.currentElement.offsetWidth || !this.currentElement.offsetHeight ||
        !elementDoc.defaultView || !elementDoc.documentElement)
    {
      this._hideTab();
      return;
    }

    let objRect = this._getElementPosition(this.currentElement);

    let className = this.objTabClassVisibleTop;
    let left = objRect.right - this.objtabElement.offsetWidth;
    let top = objRect.top - this.objtabElement.offsetHeight;
    if (top < 0)
    {
      top = objRect.bottom;
      className = this.objTabClassVisibleBottom;
    }

    if (this.objtabElement.style.left != left + "px")
      this.objtabElement.style.setProperty("left", left + "px", "important");
    if (this.objtabElement.style.top != top + "px")
      this.objtabElement.style.setProperty("top", top + "px", "important");

    if (this.objtabElement.getAttribute("class") != className)
      this.objtabElement.setAttribute("class", className);

    this.prevPositionUpdate = Date.now();
  },

  /**
   * Calculates element's position relative to the top frame and considering
   * clipping due to scrolling.
   * @return {left: Number, top: Number, right: Number, bottom: Number}
   */
  _getElementPosition: function(/**Element*/ element)
  {
    // Restrict rectangle coordinates by the boundaries of a window's client area
    function intersectRect(rect, wnd)
    {
      // Cannot use wnd.innerWidth/Height because they won't account for scrollbars
      let doc = wnd.document;
      let wndWidth = doc.documentElement.clientWidth;
      let wndHeight = doc.documentElement.clientHeight;
      if (doc.compatMode == "BackCompat") // clientHeight will be bogus in quirks mode
        wndHeight = Math.max(doc.documentElement.offsetHeight, doc.body.offsetHeight) - wnd.scrollMaxY - 1;

      rect.left = Math.max(rect.left, 0);
      rect.top = Math.max(rect.top, 0);
      rect.right = Math.min(rect.right, wndWidth);
      rect.bottom = Math.min(rect.bottom, wndHeight);
    }

    let rect = element.getBoundingClientRect();
    let wnd = element.ownerDocument.defaultView;

    let style = wnd.getComputedStyle(element, null);
    let offsets = [
      parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft),
      parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop),
      parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight),
      parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom)
    ];

    rect = {left: rect.left + offsets[0], top: rect.top + offsets[1],
            right: rect.right - offsets[2], bottom: rect.bottom - offsets[3]};
    while (true)
    {
      intersectRect(rect, wnd);

      if (!wnd.frameElement)
        break;

      // Recalculate coordinates to be relative to frame's parent window
      let frameElement = wnd.frameElement;
      wnd = frameElement.ownerDocument.defaultView;

      let frameRect = frameElement.getBoundingClientRect();
      let frameStyle = wnd.getComputedStyle(frameElement, null);
      let relLeft = frameRect.left + parseFloat(frameStyle.borderLeftWidth) + parseFloat(frameStyle.paddingLeft);
      let relTop = frameRect.top + parseFloat(frameStyle.borderTopWidth) + parseFloat(frameStyle.paddingTop);

      rect.left += relLeft;
      rect.right += relLeft;
      rect.top += relTop;
      rect.bottom += relTop;
    }

    return rect;
  },

  doBlock: function()
  {
    let {UI} = require("ui");
    let {Utils} = require("utils");
    let chromeWindow = Utils.getChromeWindow(this.currentElement.ownerDocument.defaultView);
    UI.blockItem(chromeWindow, this.currentElement, this.objtabElement.nodeData);
  },

  /**
   * Called whenever a timer fires.
   * @param {nsISupport} subject
   * @param {string} topic
   * @param {string} data
   */
  observe: function(subject, topic, data)
  {
    if (subject == this.positionTimer)
    {
      // Don't update position if it was already updated recently (via MozAfterPaint)
      if (Date.now() - this.prevPositionUpdate > 100)
        this._positionTab();
    }
    else if (subject == this.hideTimer)
    {
      let now = Date.now();
      if (now >= this.hideTargetTime)
        this._hideTab();
      else if (this.hideTargetTime - now < this.HIDE_DELAY / 2)
        this.objtabElement.style.setProperty("opacity", (this.hideTargetTime - now) * 2 / this.HIDE_DELAY, "important");
    }
  }
};

onShutdown.add(objTabs._hideTab.bind(objTabs));

/**
 * Function called whenever the mouse enters or leaves an object.
 */
function objectMouseEventHander(/**Event*/ event)
{
  if (!event.isTrusted)
    return;

  if (event.type == "mouseover")
    objTabs.showTabFor(event.target);
  else if (event.type == "mouseout")
    objTabs.hideTabFor(event.target);
}

/**
 * Function called for paint events of the object tab window.
 */
function objectWindowEventHandler(/**Event*/ event)
{
  if (!event.isTrusted)
    return;

  // Don't trigger update too often, avoid overusing CPU on frequent page updates
  if (event.type == "MozAfterPaint" && Date.now() - objTabs.prevPositionUpdate > 20)
    objTabs._positionTab();
}

/**
 * Function called whenever the mouse enters or leaves an object tab.
 */
function objectTabEventHander(/**Event*/ event)
{
  if (onShutdown.done || !event.isTrusted)
    return;

  if (event.type == "click" && event.button == 0)
  {
    event.preventDefault();
    event.stopPropagation();

    objTabs.doBlock();
  }
  else if (event.type == "mouseover")
    objTabs.showTabFor(objTabs.currentElement);
  else if (event.type == "mouseout")
    objTabs.hideTabFor(objTabs.currentElement);
}
exports.objectMouseEventHander = objectMouseEventHander;
