/*
 * This file is part of SpyBlock <http://www.gnu.org/software/gnuzilla/>,
 * Copyright (C) 2006-2014 Eyeo GmbH (Adblock Plus) 
 * Copyright (C) 2014 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/>.
 */

//
// Report data template, more data will be added during data collection
//

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");

const MILLISECONDS_IN_SECOND = 1000;
const SECONDS_IN_MINUTE = 60;
const SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE;
const SECONDS_IN_DAY = 24 * SECONDS_IN_HOUR;

let contentWindow = window.arguments[0];
let windowURI = (window.arguments[1] instanceof Ci.nsIURI ? window.arguments[1] : null);

let reportData = new DOMParser().parseFromString("<report></report>", "text/xml");

// Some helper functions to work with the report data
function reportElement(tag)
{
  for (let child = reportData.documentElement.firstChild; child; child = child.nextSibling)
    if (child.nodeType == Node.ELEMENT_NODE && child.tagName == tag)
      return child;
  let element = reportData.createElement(tag);
  reportData.documentElement.appendChild(element);
  return element;
}
function removeReportElement(tag)
{
  for (let child = reportData.documentElement.firstChild; child; child = child.nextSibling)
    if (child.nodeType == Node.ELEMENT_NODE && child.tagName == tag)
      child.parentNode.removeChild(child);
}
function appendElement(parent, tag, attributes, body)
{
  let element = parent.ownerDocument.createElement(tag);
  if (typeof attributes == "object" && attributes !== null)
    for (let attribute in attributes)
      if (attributes.hasOwnProperty(attribute))
        element.setAttribute(attribute, attributes[attribute]);
  if (typeof body != "undefined" && body !== null)
    element.textContent = body;
  parent.appendChild(element);
  return element;
}
function serializeReportData()
{
  let result = new XMLSerializer().serializeToString(reportData);

  // Insert line breaks before each new tag
  result = result.replace(/(<[^\/]([^"<>]*|"[^"]*")*>)/g, "\n$1");
  result = result.replace(/^\n+/, "");
  return result;
}

let (element = reportElement("adblock-plus"))
{
  let {addonVersion} = require("info");
  element.setAttribute("version", addonVersion);
  element.setAttribute("locale", Utils.appLocale);
};
let (element = reportElement("application"))
{
  element.setAttribute("name", Services.appinfo.name);
  element.setAttribute("vendor", Services.appinfo.vendor);
  element.setAttribute("version", Services.appinfo.version);
  element.setAttribute("userAgent", window.navigator.userAgent);
};
let (element = reportElement("platform"))
{
  element.setAttribute("name", "Gecko");
  element.setAttribute("version", Services.appinfo.platformVersion);
  element.setAttribute("build", Services.appinfo.platformBuildID);
};
let (element = reportElement("options"))
{
  appendElement(element, "option", {id: "enabled"}, Prefs.enabled);
  appendElement(element, "option", {id: "objecttabs"}, Prefs.frameobjects);
  appendElement(element, "option", {id: "collapse"}, !Prefs.fastcollapse);
  appendElement(element, "option", {id: "privateBrowsing"}, PrivateBrowsing.enabledForWindow(contentWindow) || PrivateBrowsing.enabled);
  appendElement(element, "option", {id: "subscriptionsAutoUpdate"}, Prefs.subscriptions_autoupdate);
  appendElement(element, "option", {id: "javascript"}, Services.prefs.getBoolPref("javascript.enabled"));
  appendElement(element, "option", {id: "cookieBehavior"}, Services.prefs.getIntPref("network.cookie.cookieBehavior"));
};

//
// Data collectors
//

let reportsListDataSource =
{
  list: [],

  collectData: function(wnd, windowURI, callback)
  {
    let data = Prefs.recentReports;
    if (data && "length" in data)
    {
      for (let i = 0; i < data.length; i++)
      {
        let entry = data[i];
        if (typeof entry.reportURL == "string" && entry.reportURL &&
            typeof entry.time == "number" && Date.now() - entry.time < 30*24*60*60*1000)
        {
          let newEntry = {site: null, reportURL: entry.reportURL, time: entry.time};
          if (typeof entry.site == "string" && entry.site)
            newEntry.site = entry.site;
          this.list.push(newEntry);
        }
      }
    }

    if (this.list.length > 10)
      this.list.splice(10);

    E("recentReports").hidden = !this.list.length;
    if (this.list.length)
    {
      let rows = E("recentReportsRows")
      for (let i = 0; i < this.list.length; i++)
      {
        let entry = this.list[i];
        let row = document.createElement("row");

        let link = document.createElement("description");
        link.setAttribute("class", "text-link");
        link.setAttribute("url", entry.reportURL);
        link.textContent = entry.reportURL.replace(/^.*\/(?=[^\/])/, "");
        row.appendChild(link);

        let site = document.createElement("description");
        if (entry.site)
          site.textContent = entry.site;
        row.appendChild(site);

        let time = document.createElement("description");
        time.textContent = Utils.formatTime(entry.time);
        row.appendChild(time);

        rows.appendChild(row);
      }
    }

    callback();
  },

  addReport: function(site, reportURL)
  {
    this.list.unshift({site: site, reportURL: reportURL, time: Date.now()});
    Prefs.recentReports = this.list;
  },

  clear: function()
  {
    this.list = [];
    Prefs.recentReports = this.list;
    E("recentReports").hidden = true;
  },

  handleClick: function(event)
  {
    if (event.button != 0 || !event.target || !event.target.hasAttribute("url"))
      return;

    UI.loadInBrowser(event.target.getAttribute("url"));
  }
};

let requestsDataSource =
{
  requests: reportElement("requests"),
  origRequests: [],
  requestNotifier: null,
  callback: null,
  nodeByKey: {__proto__: null},

  collectData: function(wnd, windowURI, callback)
  {
    this.callback = callback;
    this.requestNotifier = new RequestNotifier(wnd, this.onRequestFound, this);
  },

  onRequestFound: function(frame, node, entry, scanComplete)
  {
    if (entry)
    {
      let key = entry.location + " " + entry.typeDescr + " " + entry.docDomain;
      let requestXML;
      if (key in this.nodeByKey)
      {
        requestXML = this.nodeByKey[key];
        requestXML.setAttribute("count", parseInt(requestXML.getAttribute("count"), 10) + 1);
      }
      else
      {
        requestXML = this.nodeByKey[key] = appendElement(this.requests, "request", {
          location: censorURL(entry.location),
          type: entry.typeDescr,
          docDomain: entry.docDomain,
          thirdParty: entry.thirdParty,
          count: 1
        });
      }

      // Location is meaningless for element hiding hits
      if (entry.filter && entry.filter instanceof ElemHideBase)
        requestXML.removeAttribute("location");

      if (entry.filter)
        requestXML.setAttribute("filter", entry.filter.text);

      if (node instanceof Element)
      {
        requestXML.setAttribute("node", (node.namespaceURI ? node.namespaceURI + "#" : "") + node.localName);

        try
        {
          requestXML.setAttribute("size", node.offsetWidth + "x" + node.offsetHeight);
        } catch(e) {}
      }
      this.origRequests.push(entry);
    }

    if (scanComplete)
    {
      this.requestNotifier.shutdown();
      this.requestNotifier = null;
      this.callback();
    }
  }
};

let filtersDataSource =
{
  origFilters: [],

  collectData: function(wnd, windowURI, callback)
  {
    let wndStats = RequestNotifier.getWindowStatistics(wnd);
    if (wndStats)
    {
      let filters = reportElement("filters");
      for (let f in wndStats.filters)
      {
        let filter = Filter.fromText(f)
        let hitCount = wndStats.filters[f];
        appendElement(filters, "filter", {
          text: filter.text,
          subscriptions: filter.subscriptions.filter(subscriptionsDataSource.subscriptionFilter).map(function(s) s.url).join(" "),
          hitCount: hitCount
        });
        this.origFilters.push(filter);
      }
    }
    callback();
  }
};

let subscriptionsDataSource =
{
  subscriptionFilter: function(s)
  {
    if (s.disabled || !(s instanceof RegularSubscription))
      return false;
    if (s instanceof DownloadableSubscription && !/^(http|https|ftp):/i.test(s.url))
      return false;
    return true;
  },

  collectData: function(wnd, windowURI, callback)
  {
    let subscriptions = reportElement("subscriptions");
    let now = Math.round(Date.now() / 1000);
    for (let i = 0; i < FilterStorage.subscriptions.length; i++)
    {
      let subscription = FilterStorage.subscriptions[i];
      if (!this.subscriptionFilter(subscription))
        continue;

      let subscriptionXML = appendElement(subscriptions, "subscription", {
        id: subscription.url,
        disabledFilters: subscription.filters.filter(function(filter) filter instanceof ActiveFilter && filter.disabled).length
      });
      if (subscription.version)
        subscriptionXML.setAttribute("version", subscription.version);
      if (subscription.lastDownload)
        subscriptionXML.setAttribute("lastDownloadAttempt", subscription.lastDownload - now);
      if (subscription instanceof DownloadableSubscription)
      {
        if (subscription.lastSuccess)
          subscriptionXML.setAttribute("lastDownloadSuccess", subscription.lastSuccess - now);
        if (subscription.softExpiration)
          subscriptionXML.setAttribute("softExpiration", subscription.softExpiration - now);
        if (subscription.expires)
          subscriptionXML.setAttribute("hardExpiration", subscription.expires - now);
        subscriptionXML.setAttribute("downloadStatus", subscription.downloadStatus);
      }
    }
    callback();
  }
};

let screenshotDataSource =
{
  imageOffset: 10,

  // Fields used for color reduction
  _mapping: [0x00,  0x55,  0xAA,  0xFF],
  _i: null,
  _max: null,
  _pixelData: null,
  _callback: null,

  // Fields used for user interaction
  _enabled: true,
  _canvas: null,
  _context: null,
  _selectionType: "mark",
  _currentData: null,
  _undoQueue: [],

  collectData: function(wnd, windowURI, callback)
  {
    this._callback = callback;
    this._canvas = E("screenshotCanvas");
    this._canvas.width = this._canvas.offsetWidth;

    // Do not resize canvas any more (no idea why Gecko requires both to be set)
    this._canvas.parentNode.style.MozBoxAlign = "center";
    this._canvas.parentNode.align = "center";

    this._context = this._canvas.getContext("2d");
    let wndWidth = wnd.document.documentElement.scrollWidth;
    let wndHeight = wnd.document.documentElement.scrollHeight;

    // Copy scaled screenshot of the webpage. We scale the webpage by width
    // but leave 10px on each side for easier selecting.

    // Gecko doesn't like sizes more than 64k, restrict to 30k to be on the safe side.
    // Also, make sure height is at most five times the width to keep image size down.
    let copyWidth = Math.min(wndWidth, 30000);
    let copyHeight = Math.min(wndHeight, 30000, copyWidth * 5);
    let copyX = Math.max(Math.min(wnd.scrollX - copyWidth / 2, wndWidth - copyWidth), 0);
    let copyY = Math.max(Math.min(wnd.scrollY - copyHeight / 2, wndHeight - copyHeight), 0);

    let scalingFactor = (this._canvas.width - this.imageOffset * 2) / copyWidth;
    this._canvas.height = copyHeight * scalingFactor + this.imageOffset * 2;

    this._context.save();
    this._context.translate(this.imageOffset, this.imageOffset);
    this._context.scale(scalingFactor, scalingFactor);
    this._context.drawWindow(wnd, copyX, copyY, copyWidth, copyHeight, "rgb(255,255,255)");
    this._context.restore();

    // Init canvas settings
    this._context.fillStyle = "rgb(0, 0, 0)";
    this._context.strokeStyle = "rgba(255, 0, 0, 0.7)";
    this._context.lineWidth = 3;
    this._context.lineJoin = "round";

    // Reduce colors asynchronously
    this._pixelData = this._context.getImageData(this.imageOffset, this.imageOffset,
                                      this._canvas.width - this.imageOffset * 2,
                                      this._canvas.height - this.imageOffset * 2);
    this._max = this._pixelData.width * this._pixelData.height * 4;
    this._i = 0;
    Utils.runAsync(this.run.bind(this));
  },

  run: function()
  {
    // Process only 5000 bytes at a time to prevent browser hangs
    let endIndex = Math.min(this._i + 5000, this._max);
    let i = this._i;
    for (; i < endIndex; i++)
      this._pixelData.data[i] = this._mapping[this._pixelData.data[i] >> 6];

    if (i >= this._max)
    {
      // Save data back and we are done
      this._context.putImageData(this._pixelData, this.imageOffset, this.imageOffset);
      this._callback();
    }
    else
    {
      this._i = i;
      Utils.runAsync(this.run.bind(this));
    }
  },

  get enabled() this._enabled,
  set enabled(enabled)
  {
    if (this._enabled == enabled)
      return;

    this._enabled = enabled;
    this._canvas.style.opacity = this._enabled ? "" : "0.3"
    E("screenshotMarkButton").disabled = !this._enabled;
    E("screenshotRemoveButton").disabled = !this._enabled;
    E("screenshotUndoButton").disabled = !this._enabled || !this._undoQueue.length;
  },

  get selectionType() this._selectionType,
  set selectionType(type)
  {
    if (this._selectionType == type)
      return;

    // Abort selection already in progress
    this.abortSelection();

    this._selectionType = type;
  },

  exportData: function()
  {
    removeReportElement("screenshot");
    if (this.enabled)
    {
      appendElement(reportData.documentElement, "screenshot", {
        edited: (this._undoQueue.length ? 'true' : 'false')
      }, this._canvas.toDataURL());
    }
  },

  abortSelection: function()
  {
    if (this._currentData && this._currentData.data)
    {
      this._context.putImageData(this._currentData.data,
        Math.min(this._currentData.anchorX, this._currentData.currentX),
        Math.min(this._currentData.anchorY, this._currentData.currentY));
    }
    document.removeEventListener("keypress", this.handleKeyPress, true);
    this._currentData = null;
  },

  handleKeyPress: function(event)
  {
    if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE)
    {
      event.stopPropagation();
      event.preventDefault();
      screenshotDataSource.abortSelection();
    }
  },

  startSelection: function(event)
  {
    if (event.button == 2)
      this.abortSelection();   // Right mouse button aborts selection

    if (event.button != 0 || !this.enabled)
      return;

    // Abort selection already in progress
    this.abortSelection();

    let boxObject = document.getBoxObjectFor(this._canvas);
    let [x, y] = [event.screenX - boxObject.screenX, event.screenY - boxObject.screenY];
    this._currentData = {
      data: null,
      anchorX: x,
      anchorY: y,
      currentX: -1,
      currentY: -1
    };
    this.updateSelection(event);

    document.addEventListener("keypress", this.handleKeyPress, true);
  },

  updateSelection: function(event)
  {
    if (event.button != 0 || !this._currentData)
      return;

    let boxObject = document.getBoxObjectFor(this._canvas);
    let [x, y] = [event.screenX - boxObject.screenX, event.screenY - boxObject.screenY];
    if (this._currentData.currentX == x && this._currentData.currentY == y)
      return;

    if (this._currentData.data)
    {
      this._context.putImageData(this._currentData.data,
        Math.min(this._currentData.anchorX, this._currentData.currentX),
        Math.min(this._currentData.anchorY, this._currentData.currentY));
    }

    this._currentData.currentX = x;
    this._currentData.currentY = y;

    let left = Math.min(this._currentData.anchorX, this._currentData.currentX);
    let right = Math.max(this._currentData.anchorX, this._currentData.currentX);
    let top = Math.min(this._currentData.anchorY, this._currentData.currentY);
    let bottom = Math.max(this._currentData.anchorY, this._currentData.currentY);

    let minDiff = (this._selectionType == "mark" ? 3 : 1);
    if (right - left >= minDiff && bottom - top >= minDiff)
      this._currentData.data = this._context.getImageData(left, top, right - left, bottom - top);
    else
      this._currentData.data = null;

    if (this._selectionType == "mark")
    {
      // all coordinates need to be moved 1.5px inwards to get the desired result
      left += 1.5;
      right -= 1.5;
      top += 1.5;
      bottom -= 1.5;
      if (left < right && top < bottom)
        this._context.strokeRect(left, top, right - left, bottom - top);
    }
    else if (this._selectionType == "remove")
      this._context.fillRect(left, top, right - left, bottom - top);
  },

  stopSelection: function(event)
  {
    if (event.button != 0 || !this._currentData)
      return;

    if (this._currentData.data)
    {
      this._undoQueue.push(this._currentData);
      E("screenshotUndoButton").disabled = false;
    }

    this._currentData = null;
    document.removeEventListener("keypress", this.handleKeyPress, true);
  },

  undo: function()
  {
    let op = this._undoQueue.pop();
    if (!op)
      return;

    this._context.putImageData(op.data,
      Math.min(op.anchorX, op.currentX),
      Math.min(op.anchorY, op.currentY));

    if (!this._undoQueue.length)
      E("screenshotUndoButton").disabled = true;
  }
};

let framesDataSource =
{
  site: null,

  collectData: function(wnd, windowURI, callback)
  {
    try
    {
      this.site = windowURI.host;
      if (this.site)
        document.title += " (" + this.site + ")";
    }
    catch (e)
    {
      // Expected exception - not all URL schemes have a host name
    }

    let window = reportElement("window");
    window.setAttribute("url", censorURL(windowURI ? windowURI.spec : wnd.location.href));
    if (wnd.opener && wnd.opener.location.href)
      window.setAttribute("opener", censorURL(wnd.opener.location.href));
    if (wnd.document.referrer)
      window.setAttribute("referrer", censorURL(wnd.document.referrer));
    this.scanFrames(wnd, window);

    callback();
  },

  scanFrames: function(wnd, xmlList)
  {
    try
    {
      for (let i = 0; i < wnd.frames.length; i++)
      {
        let frame = wnd.frames[i];
        let frameXML = appendElement(xmlList, "frame", {
          url: censorURL(frame.location.href)
        });
        this.scanFrames(frame, frameXML);
      }
    }
    catch (e)
    {
      // Don't break if something goes wrong
      Cu.reportError(e);
    }
  }
};

let errorsDataSource =
{
  collectData: function(wnd, windowURI, callback)
  {
    let {addonID} = require("info");
    addonID = addonID.replace(/[\{\}]/g, "");

    // See https://bugzilla.mozilla.org/show_bug.cgi?id=664695 - starting with
    // Gecko 19 this function returns the result, before that it wrote to a
    // parameter.
    let outparam = {};
    let messages = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).getMessageArray(outparam, {});
    messages = messages || outparam.value || [];
    messages = messages.filter(function(message)
    {
      return (message instanceof Ci.nsIScriptError &&
          !/^https?:/i.test(message.sourceName) &&
          (/adblock/i.test(message.errorMessage) || /adblock/i.test(message.sourceName) ||
           message.errorMessage.indexOf(addonID) >= 0 || message.sourceName && message.sourceName.indexOf(addonID) >= 0));
    });
    if (messages.length > 10)   // Only the last 10 messages
      messages = messages.slice(messages.length - 10, messages.length);

    // Censor app and profile paths in error messages
    let censored = {__proto__: null};
    let pathList = [["ProfD", "%PROFILE%"], ["GreD", "%GRE%"], ["CurProcD", "%APP%"]];
    for (let i = 0; i < pathList.length; i++)
    {
      let [pathID, placeholder] = pathList[i];
      try
      {
        let file = FileUtils.getDir(pathID, [], false);
        censored[file.path.replace(/[\\\/]+$/, '')] = placeholder;
        let uri = Utils.ioService.newFileURI(file);
        censored[uri.spec.replace(/[\\\/]+$/, '')] = placeholder;
      } catch(e) {}
    }

    let errors = reportElement("errors");
    for (let i = 0; i < messages.length; i++)
    {
      let message = messages[i];

      let text = message.errorMessage;
      for (let path in censored)
        text = text.replace(path, censored[path], "gi");
      if (text.length > 256)
        text = text.substr(0, 256) + "...";

      let file = message.sourceName;
      for (let path in censored)
        file = file.replace(path, censored[path], "gi");
      if (file.length > 256)
        file = file.substr(0, 256) + "...";

      let sourceLine = message.sourceLine;
      if (sourceLine.length > 256)
        sourceLine = sourceLine.substr(0, 256) + "...";

      appendElement(errors, "error", {
        type: message.flags & Ci.nsIScriptError.warningFlag ? "warning" : "error",
        text: text,
        file: file,
        line: message.lineNumber,
        column: message.columnNumber,
        sourceLine: sourceLine
      });
    }

    callback();
  }
};

let extensionsDataSource =
{
  data: reportData.createElement("extensions"),

  collectData: function(wnd, windowURI, callback)
  {
    try
    {
      let AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", null).AddonManager;
      AddonManager.getAddonsByTypes(["extension", "plugin"], function(items)
      {
        for (let i = 0; i < items.length; i++)
        {
          let item = items[i];
          if (!item.isActive)
            continue;
          appendElement(this.data, "extension", {
            id: item.id,
            name: item.name,
            type: item.type,
            version: item.version
          });
        }
        callback();
      }.bind(this));
    }
    catch (e)
    {
      // No add-on manager, what's going on? Skip this step.
      callback();
    }
  },

  exportData: function(doExport)
  {
    if (doExport)
      reportData.documentElement.appendChild(this.data);
    else if (this.data.parentNode)
      this.data.parentNode.removeChild(this.data);
  }
};

let subscriptionUpdateDataSource =
{
  contentWnd: null,
  type: null,
  outdated: null,
  needUpdate: null,

  collectData: function(wnd, windowURI, callback)
  {
    this.contentWnd = wnd;
    let now = Date.now() / MILLISECONDS_IN_SECOND;
    let outdatedThreshold = now - 14 * SECONDS_IN_DAY;
    let needUpdateThreshold = now - 1 * SECONDS_IN_HOUR;

    this.outdated = [];
    this.needUpdate = [];

    let subscriptions = FilterStorage.subscriptions.filter(issuesDataSource.subscriptionFilter);
    for (let i = 0; i < subscriptions.length; i++)
    {
      let lastSuccess = subscriptions[i].lastSuccess;
      if (lastSuccess < outdatedThreshold)
        this.outdated.push(subscriptions[i]);
      if (lastSuccess < needUpdateThreshold)
        this.needUpdate.push(subscriptions[i]);
    }

    callback();
  },

  updatePage: function(type)
  {
    this.type = type;
    E("updateInProgress").hidden = (type != "false positive" || this.needUpdate.length == 0);
    E("outdatedSubscriptions").hidden = !E("updateInProgress").hidden || this.outdated.length == 0;
    if (!E("outdatedSubscriptions").hidden)
    {
      let template = E("outdatedSubscriptionTemplate");
      let list = E("outdatedSubscriptionsList");
      while (list.lastChild)
        list.removeChild(list.lastChild);

      for (let i = 0; i < this.outdated.length; i++)
      {
        let subscription = this.outdated[i];
        let entry = template.cloneNode(true);
        entry.removeAttribute("id");
        entry.removeAttribute("hidden");
        entry.setAttribute("_url", subscription.url);
        entry.setAttribute("tooltiptext", subscription.url);
        entry.textContent = subscription.title;
        list.appendChild(entry);
      }
    }
    return !E("updateInProgress").hidden || !E("outdatedSubscriptions").hidden;
  },

  showPage: function()
  {
    document.documentElement.canAdvance = false;

    if (!E("updateInProgress").hidden)
    {
      document.documentElement.canRewind = false;

      for (let i = 0; i < this.needUpdate.length; i++)
        Synchronizer.execute(this.needUpdate[i], true);

      let listener = function(action)
      {
        if (!/^subscription\./.test(action))
          return;

        for (let i = 0; i < this.needUpdate.length; i++)
          if (Synchronizer.isExecuting(this.needUpdate[i].url))
            return;

        FilterNotifier.removeListener(listener);
        E("updateInProgress").hidden = "true";

        let filtersRemoved = false;
        let requests = requestsDataSource.origRequests;
        for (let i = 0; i < requests.length; i++)
          if (requests[i].filter && !requests[i].filter.subscriptions.filter(function(s) !s.disabled).length)
            filtersRemoved = true;

        if (filtersRemoved)
        {
          // Force the user to reload the page
          E("updateFixedIssue").hidden = false;
          document.documentElement.canAdvance = true;

          let nextButton = document.documentElement.getButton("next");
          [nextButton.label, nextButton.accessKey] = Utils.splitLabel(E("updatePage").getAttribute("reloadButtonLabel"));
          document.documentElement.addEventListener("wizardnext", function(event)
          {
            event.preventDefault();
            event.stopPropagation();
            window.close();
            this.contentWnd.location.reload();
          }.bind(this), true);
        }
        else
        {
          this.collectData(null, null, function() {});
          this.needUpdate = [];
          if (this.outdated.length)
          {
            document.documentElement.canRewind = true;

            this.updatePage(this.type);
            this.showPage();
          }
          else
          {
            // No more issues, make sure to remove this page from history and
            // advance to the next page.
            document.documentElement.canRewind = true;
            document.documentElement.canAdvance = true;

            let next = document.documentElement.currentPage.next;
            document.documentElement.rewind();
            document.documentElement.currentPage.next = next;

            document.documentElement.advance();
          }
        }
      }.bind(this);

      FilterNotifier.addListener(listener);
      window.addEventListener("unload", function()
      {
        FilterNotifier.removeListener(listener);
      });
    }
  },

  updateOutdated: function()
  {
    for (let i = 0; i < this.outdated.length; i++)
      Synchronizer.execute(this.outdated[i], true);
  }
}

let issuesDataSource =
{
  contentWnd: null,
  isEnabled: Prefs.enabled,
  whitelistFilter: null,
  disabledFilters: [],
  disabledSubscriptions: [],
  ownFilters: [],
  numSubscriptions: 0,
  numAppliedFilters: Infinity,

  subscriptionFilter: function(s)
  {
    if (s instanceof DownloadableSubscription)
      return subscriptionsDataSource.subscriptionFilter(s);
    else
      return false;
  },

  collectData: function(wnd, windowURI, callback)
  {
    this.contentWnd = wnd;
    this.whitelistFilter = Policy.isWindowWhitelisted(wnd);

    if (!this.whitelistFilter && this.isEnabled)
    {
      // Find disabled filters in active subscriptions matching any of the requests
      let disabledMatcher = new CombinedMatcher();
      for each (let subscription in FilterStorage.subscriptions)
      {
        if (subscription.disabled)
          continue;

        for each (let filter in subscription.filters)
          if (filter instanceof BlockingFilter && filter.disabled)
            disabledMatcher.add(filter);
      }

      let seenFilters = {__proto__: null};
      for each (let request in requestsDataSource.origRequests)
      {
        if (request.filter)
          continue;

        let filter = disabledMatcher.matchesAny(request.location, request.typeDescr, request.docDomain, request.thirdParty);
        if (filter && !(filter.text in seenFilters))
        {
          this.disabledFilters.push(filter);
          seenFilters[filter.text] = true;
        }
      }

      // Find disabled subscriptions with filters matching any of the requests
      let seenSubscriptions = {__proto__: null};
      for each (let subscription in FilterStorage.subscriptions)
      {
        if (!subscription.disabled)
          continue;

        disabledMatcher.clear();
        for each (let filter in subscription.filters)
          if (filter instanceof BlockingFilter)
            disabledMatcher.add(filter);

        for each (let request in requestsDataSource.origRequests)
        {
          if (request.filter)
            continue;

          let filter = disabledMatcher.matchesAny(request.location, request.typeDescr, request.docDomain, request.thirdParty);
          if (filter && !(subscription.url in seenSubscriptions))
          {
            this.disabledSubscriptions.push(subscription);
            seenSubscriptions[subscription.text] = true;
            break;
          }
        }
      }

      this.numSubscriptions = FilterStorage.subscriptions.filter(this.subscriptionFilter).length;
      this.numAppliedFilters = 0;
      for each (let filter in filtersDataSource.origFilters)
      {
        if (filter instanceof WhitelistFilter)
          continue;

        this.numAppliedFilters++;
        if (filter.subscriptions.some(function(subscription) subscription instanceof SpecialSubscription))
          this.ownFilters.push(filter);
      }
    }

    callback();
  },

  updateIssues: function(type)
  {
    if (type == "other")
    {
      E("typeSelectorPage").next = "typeWarning";
      return;
    }

    E("issuesWhitelistBox").hidden = !this.whitelistFilter;
    E("issuesDisabledBox").hidden = this.isEnabled;
    E("issuesNoFiltersBox").hidden = (type != "false positive" || this.numAppliedFilters > 0);
    E("issuesNoSubscriptionsBox").hidden = (type != "false negative" || this.numAppliedFilters > 0 || this.numSubscriptions > 0);
    E("issuesSubscriptionCountBox").hidden = (this.numSubscriptions < 5);

    let ownFiltersBox = E("issuesOwnFilters");
    if (this.ownFilters.length && !ownFiltersBox.firstChild)
    {
      let template = E("issuesOwnFiltersTemplate");
      for each (let filter in this.ownFilters)
      {
        let element = template.cloneNode(true);
        element.removeAttribute("id");
        element.removeAttribute("hidden");
        element.firstChild.setAttribute("value", filter.text);
        element.firstChild.setAttribute("tooltiptext", filter.text);
        element.abpFilter = filter;
        ownFiltersBox.appendChild(element);
      }
    }
    E("issuesOwnFiltersBox").hidden = (type != "false positive" || this.ownFilters.length == 0);

    let disabledSubscriptionsBox = E("issuesDisabledSubscriptions");
    if (this.disabledSubscriptions.length && !disabledSubscriptionsBox.firstChild)
    {
      let template = E("issuesDisabledSubscriptionsTemplate");
      for each (let subscription in this.disabledSubscriptions)
      {
        let element = template.cloneNode(true);
        element.removeAttribute("id");
        element.removeAttribute("hidden");
        element.firstChild.setAttribute("value", subscription.title);
        element.setAttribute("tooltiptext", subscription instanceof DownloadableSubscription ? subscription.url : subscription.title);
        element.abpSubscription = subscription;
        disabledSubscriptionsBox.appendChild(element);
      }
    }
    E("issuesDisabledSubscriptionsBox").hidden = (type != "false negative" || this.disabledSubscriptions.length == 0);

    let disabledFiltersBox = E("issuesDisabledFilters");
    if (this.disabledFilters.length && !disabledFiltersBox.firstChild)
    {
      let template = E("issuesDisabledFiltersTemplate");
      for each (let filter in this.disabledFilters)
      {
        let element = template.cloneNode(true);
        element.removeAttribute("id");
        element.removeAttribute("hidden");
        element.firstChild.setAttribute("value", filter.text);
        element.setAttribute("tooltiptext", filter.text);
        element.abpFilter = filter;
        disabledFiltersBox.appendChild(element);
      }
    }
    E("issuesDisabledFiltersBox").hidden = (type != "false negative" || this.disabledFilters.length == 0);

    // Don't allow sending report if the page is whitelisted - we need the data.
    // Also disallow reports without matching filters or without subscriptions,
    // subscription authors cannot do anything about those.
    E("issuesOverride").hidden = !E("issuesWhitelistBox").hidden ||
                                 !E("issuesDisabledBox").hidden ||
                                 !E("issuesNoFiltersBox").hidden ||
                                 !E("issuesNoSubscriptionsBox").hidden ||
                                 !E("issuesSubscriptionCountBox").hidden;

    let page = E("typeSelectorPage");
    if (subscriptionUpdateDataSource.updatePage(type))
    {
      page.next = "update";
      page = E("updatePage");
    }

    if (E("issuesWhitelistBox").hidden && E("issuesDisabledBox").hidden &&
        E("issuesNoFiltersBox").hidden && E("issuesNoSubscriptionsBox").hidden &&
        E("issuesOwnFiltersBox").hidden && E("issuesDisabledFiltersBox").hidden &&
        E("issuesDisabledSubscriptionsBox").hidden && E("issuesSubscriptionCountBox").hidden)
    {
      page.next = "screenshot";
    }
    else
    {
      page.next = "issues";
    }
  },

  forceReload: function()
  {
    // User changed configuration, don't allow sending report now - page needs
    // to be reloaded
    E("issuesOverride").hidden = true;
    E("issuesChangeMessage").hidden = false;
    document.documentElement.canRewind = false;
    document.documentElement.canAdvance = true;

    let contentWnd = this.contentWnd;
    let nextButton = document.documentElement.getButton("next");
    [nextButton.label, nextButton.accessKey] = Utils.splitLabel(E("updatePage").getAttribute("reloadButtonLabel"));
    document.documentElement.addEventListener("wizardnext", function(event)
    {
      event.preventDefault();
      event.stopPropagation();
      window.close();
      contentWnd.location.reload();
    }, true);
  },

  removeWhitelist: function()
  {
    if (this.whitelistFilter && this.whitelistFilter.subscriptions.length)
      this.whitelistFilter.disabled = true;
    E("issuesWhitelistBox").hidden = true;
    this.forceReload();
  },

  enable: function()
  {
    Prefs.enabled = true;
    E("issuesDisabledBox").hidden = true;
    this.forceReload();
  },

  addSubscription: function()
  {
    let result = {};
    openDialog("subscriptionSelection.xul", "_blank", "chrome,centerscreen,modal,resizable,dialog=no", null, result);
    if (!("url" in result))
      return;

    let subscriptionResults = [[result.url, result.title]];
    if ("mainSubscriptionURL" in result)
      subscriptionResults.push([result.mainSubscriptionURL, result.mainSubscriptionTitle]);

    for each (let [url, title] in subscriptionResults)
    {
      let subscription = Subscription.fromURL(url);
      if (!subscription)
        continue;

      FilterStorage.addSubscription(subscription);

      subscription.disabled = false;
      subscription.title = title;

      if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
        Synchronizer.execute(subscription);
    }

    E("issuesNoSubscriptionsBox").hidden = true;
    this.forceReload();
  },

  disableFilter: function(node)
  {
    let filter = node.abpFilter;
    if (filter && filter.subscriptions.length)
      filter.disabled = true;

    node.parentNode.removeChild(node);
    if (!E("issuesOwnFilters").firstChild)
      E("issuesOwnFiltersBox").hidden = true;
    this.forceReload();
  },

  enableFilter: function(node)
  {
    let filter = node.abpFilter;
    if (filter && filter.subscriptions.length)
      filter.disabled = false;

    node.parentNode.removeChild(node);
    if (!E("issuesDisabledFilters").firstChild)
      E("issuesDisabledFiltersBox").hidden = true;
    this.forceReload();
  },


  enableSubscription: function(node)
  {
    let subscription = node.abpSubscription;
    if (subscription)
      subscription.disabled = false;

    node.parentNode.removeChild(node);
    if (!E("issuesDisabledSubscriptions").firstChild)
      E("issuesDisabledSubscriptionsBox").hidden = true;
    this.forceReload();
  }
};

let dataCollectors = [reportsListDataSource, requestsDataSource, filtersDataSource, subscriptionsDataSource,
                      screenshotDataSource, framesDataSource, errorsDataSource, extensionsDataSource,
                      subscriptionUpdateDataSource, issuesDataSource];

//
// Wizard logic
//

function initWizard()
{
  // Make sure no issue type is selected by default
  E("typeGroup").selectedItem = null;
  document.documentElement.addEventListener("pageshow", updateNextButton, false);

  // Move wizard header
  let header = document.getAnonymousElementByAttribute(document.documentElement, "class", "wizard-header");
  if (header)
  {
    document.getElementById("wizardHeaderLabel").setAttribute("value", document.documentElement.wizardPages[0].getAttribute("label"));
    document.documentElement.insertBefore(document.getElementById("wizardHeader"), document.documentElement.firstChild);
    document.documentElement.addEventListener("pageshow", function()
    {
      document.getElementById("wizardHeaderDeck").selectedIndex = (document.documentElement.pageIndex == 0 ? 0 : 1);
    }, false);
  }

  // Move privacy link
  let extraButton = document.documentElement.getButton("extra1");
  extraButton.parentNode.insertBefore(E("privacyLink"), extraButton);
}

function updateNextButton()
{
  let nextButton = document.documentElement.getButton("next");
  if (!nextButton)
    return;

  if (document.documentElement.currentPage.id == "commentPage")
  {
    if (!("_origLabel" in nextButton))
    {
      nextButton._origLabel = nextButton.label;
      nextButton._origAccessKey = nextButton.accessKey;
      [nextButton.label, nextButton.accessKey] = Utils.splitLabel(document.documentElement.getAttribute("sendbuttonlabel"));
    }
  }
  else
  {
    if ("_origLabel" in nextButton)
    {
      nextButton.label = nextButton._origLabel;
      nextButton.accessKey = nextButton._origAccessKey;
      delete nextButton._origLabel;
      delete nextButton._origAccessKey;
    }
  }
}

function initDataCollectorPage()
{
  document.documentElement.canAdvance = false;

  let totalSteps = dataCollectors.length;
  let initNextDataSource = function()
  {
    if (!dataCollectors.length)
    {
      // We are done, continue to next page
      document.documentElement.canAdvance = true;
      document.documentElement.advance();
      return;
    }

    let progress = (totalSteps - dataCollectors.length) / totalSteps * 100;
    if (progress > 0)
    {
      let progressMeter = E("dataCollectorProgress");
      progressMeter.mode = "determined";
      progressMeter.value = progress;
    }

    // Continue with the next data source, asynchronously to allow progress meter to update
    let dataSource = dataCollectors.shift();
    Utils.runAsync(function()
    {
      dataSource.collectData(contentWindow, windowURI, initNextDataSource);
    });
  };

  initNextDataSource();
}

function initTypeSelectorPage()
{
  E("progressBar").activeItem = E("typeSelectorHeader");
  let header = document.getAnonymousElementByAttribute(document.documentElement, "class", "wizard-header");
  if (header)
    header.setAttribute("viewIndex", "1");

  document.documentElement.canRewind = false;
  typeSelectionUpdated();
}

function typeSelectionUpdated()
{
  let selection = E("typeGroup").selectedItem;
  document.documentElement.canAdvance = (selection != null);
  if (selection)
  {
    if (reportData.documentElement.getAttribute("type") != selection.value)
    {
      E("screenshotCheckbox").checked = (selection.value != "other");
      E("screenshotCheckbox").doCommand();
      E("extensionsCheckbox").checked = (selection.value == "other");
      E("extensionsCheckbox").doCommand();
    }
    reportData.documentElement.setAttribute("type", selection.value);

    issuesDataSource.updateIssues(selection.value);
  }
}

function initIssuesPage()
{
  updateIssuesOverride();
}

function updateIssuesOverride()
{
  document.documentElement.canAdvance = E("issuesOverride").checked;
}

function initTypeWarningPage()
{
  updateTypeWarningOverride();

  let textElement = E("typeWarningText");
  if ("abpInitialized" in textElement)
    return;

  let template = textElement.textContent.replace(/[\r\n\s]+/g, " ");

  let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
  while (textElement.firstChild && textElement.firstChild.nodeType != Node.ELEMENT_NODE)
    textElement.removeChild(textElement.firstChild);
  while (textElement.lastChild && textElement.lastChild.nodeType != Node.ELEMENT_NODE)
    textElement.removeChild(textElement.lastChild);

  if (textElement.firstChild)
    textElement.firstChild.textContent = linkText;
  textElement.insertBefore(document.createTextNode(beforeLink), textElement.firstChild);
  textElement.appendChild(document.createTextNode(afterLink));
  textElement.abpInitialized = true;
}

function updateTypeWarningOverride()
{
  document.documentElement.canAdvance = E("typeWarningOverride").checked;
}

function initScreenshotPage()
{
  document.documentElement.canAdvance = true;

  E("progressBar").activeItem = E("screenshotHeader");
}

function initCommentPage()
{
  E("progressBar").activeItem = E("commentPageHeader");

  updateEmail();

  screenshotDataSource.exportData();
  updateDataField();
}

function showDataField()
{
  E('dataDeck').selectedIndex = 1;
  updateDataField();
  E('data').focus();
}

let _dataFieldUpdateTimeout = null;

function _updateDataField()
{
  let dataField = E("data");
  let [selectionStart, selectionEnd] = [dataField.selectionStart, dataField.selectionEnd];
  dataField.value = serializeReportData();
  dataField.setSelectionRange(selectionStart, selectionEnd);
}

function updateDataField()
{
  // Don't do anything if data field is hidden
  if (E('dataDeck').selectedIndex != 1)
    return;

  if (_dataFieldUpdateTimeout)
  {
    window.clearTimeout(_dataFieldUpdateTimeout);
    _dataFieldUpdateTimeout = null;
  }

  _dataFieldUpdateTimeout = window.setTimeout(_updateDataField, 200);
}

function updateComment()
{
  removeReportElement("comment");

  let value = E("comment").value;
  appendElement(reportData.documentElement, "comment", null, value.substr(0, 1000));
  E("commentLengthWarning").setAttribute("visible", value.length > 1000);
  updateDataField();
}

function updateEmail()
{
  removeReportElement("email");

  let anonymous = E("anonymousCheckbox").checked;

  let value = E("email").value;

  // required for persist to work on textbox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=111486
  E("email").setAttribute("value", value);

  E("email").disabled = anonymous;
  E("emailLabel").disabled = anonymous;
  E("anonymityWarning").setAttribute("visible", anonymous);

  if (!anonymous)
    appendElement(reportData.documentElement, "email", null, value);

  updateDataField();

  document.documentElement.canAdvance = anonymous || /\S/.test(value);
}

function updateExtensions(attach)
{
  extensionsDataSource.exportData(attach);
  updateDataField();
}

function initSendPage()
{
  E("progressBar").activeItem = E("sendPageHeader");

  E("result").hidden = true;
  E("sendReportErrorBox").hidden = true;
  E("sendReportMessage").hidden = false;
  E("sendReportProgress").hidden = false;
  E("sendReportProgress").mode = "undetermined";

  document.documentElement.canRewind = false;
  document.documentElement.getButton("finish").disabled = true;

  let guid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString().replace(/[\{\}]/g, "");
  let url = Prefs.report_submiturl.replace(/%GUID%/g, guid).replace(/%LANG%/g, Utils.appLocale);
  let request = new XMLHttpRequest();
  request.open("POST", url);
  request.setRequestHeader("Content-Type", "text/xml");
  request.setRequestHeader("X-SpyBlock-Plus", "1");
  request.addEventListener("load", reportSent, false);
  request.addEventListener("error", reportSent, false);
  if ("upload" in request && request.upload)
    request.upload.addEventListener("progress", updateReportProgress, false);
  request.send(serializeReportData());
}

function updateReportProgress(event)
{
  if (!event.lengthComputable)
    return;

  let progress = Math.round(event.loaded / event.total * 100);
  if (progress > 0)
  {
    let progressMeter = E("sendReportProgress");
    progressMeter.mode = "determined";
    progressMeter.value = progress;
  }
}

function reportSent(event)
{
  let request = event.target;
  let success = false;
  let errorMessage = E("sendReportError").getAttribute("defaultError");
  try
  {
    let status = request.channel.status;
    if (Components.isSuccessCode(status))
    {
      success = (request.status == 200 || request.status == 0);
      errorMessage = request.status + " " + request.statusText;
    }
    else
    {
      errorMessage = "0x" + status.toString(16);

      // Try to find the name for the status code
      let exception = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
      exception.initialize(null, status, null, null, null, null);
      if (exception.name)
        errorMessage = exception.name;
    }
  } catch (e) {}

  let result = "";
  try
  {
    result = request.responseText;
  } catch (e) {}

  result = result.replace(/%CONFIRMATION%/g, encodeHTML(E("result").getAttribute("confirmationMessage")));
  result = result.replace(/%KNOWNISSUE%/g, encodeHTML(E("result").getAttribute("knownIssueMessage")));
  result = result.replace(/(<html)\b/, '$1 dir="' + window.getComputedStyle(document.documentElement, "").direction + '"');

  if (!success)
  {
    let errorElement = E("sendReportError");
    let template = errorElement.getAttribute("textTemplate").replace(/[\r\n\s]+/g, " ");

    let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
    beforeLink = beforeLink.replace(/\?1\?/g, errorMessage);
    afterLink = afterLink.replace(/\?1\?/g, errorMessage);

    while (errorElement.firstChild && errorElement.firstChild.nodeType != Node.ELEMENT_NODE)
      errorElement.removeChild(errorElement.firstChild);
    while (errorElement.lastChild && errorElement.lastChild.nodeType != Node.ELEMENT_NODE)
      errorElement.removeChild(errorElement.lastChild);

    if (errorElement.firstChild)
      errorElement.firstChild.textContent = linkText;
    errorElement.insertBefore(document.createTextNode(beforeLink), errorElement.firstChild);
    errorElement.appendChild(document.createTextNode(afterLink));

    E("sendReportErrorBox").hidden = false;
  }

  E("sendReportProgress").hidden = true;

  let frame = E("result");
  frame.hidden = false;
  frame.docShell.allowAuth = false;
  frame.docShell.allowJavascript = false;
  frame.docShell.allowMetaRedirects = false;
  frame.docShell.allowPlugins = false;
  frame.docShell.allowSubframes = false;

  frame.setAttribute("src", "data:text/html;charset=utf-8," + encodeURIComponent(result));

  E("sendReportMessage").hidden = true;

  if (success)
  {
    try
    {
      let link = request.responseXML.getElementById("link").getAttribute("href");
      let button = E("copyLink");
      button.setAttribute("url", link);
      button.removeAttribute("disabled");

      if (!PrivateBrowsing.enabledForWindow(contentWindow) && !PrivateBrowsing.enabled)
        reportsListDataSource.addReport(framesDataSource.site, link);
    } catch (e) {}
    E("copyLinkBox").hidden = false;

    document.documentElement.getButton("finish").disabled = false;
    document.documentElement.getButton("cancel").disabled = true;
    E("progressBar").activeItemComplete = true;
  }
}

function processLinkClick(event)
{
  event.preventDefault();

  let link = event.target;
  while (link && !(link instanceof HTMLAnchorElement))
    link = link.parentNode;

  if (link && (link.protocol == "http:" || link.protocol == "https:"))
    UI.loadInBrowser(link.href);
}

function copyLink(url)
{
  Utils.clipboardHelper.copyString(url);
}

function censorURL(url)
{
  return url.replace(/([?;&\/#][^?;&\/#]+?=)[^?;&\/#]+/g, "$1*");
}

function encodeHTML(str)
{
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
