/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var { PluralForm } = ChromeUtils.importESModule(
  "resource://gre/modules/PluralForm.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
  ContextualIdentityService:
    "resource://gre/modules/ContextualIdentityService.sys.mjs",
});

var gCookiesWindow = {
  _hosts: {},
  _hostOrder: [],
  _tree: null,
  _bundle: null,

  init() {
    Services.obs.addObserver(this, "cookie-changed");
    Services.obs.addObserver(this, "perm-changed");

    this._bundle = document.getElementById("bundlePreferences");
    this._tree = document.getElementById("cookiesList");

    this._populateList(true);

    document.getElementById("filter").focus();

    if (!Services.prefs.getBoolPref("privacy.userContext.enabled")) {
      document.getElementById("userContext").hidden = true;
      document.getElementById("userContextLabel").hidden = true;
    }
  },

  uninit() {
    Services.obs.removeObserver(this, "cookie-changed");
    Services.obs.removeObserver(this, "perm-changed");
  },

  _populateList(aInitialLoad) {
    this._loadCookies();
    this._tree.view = this._view;
    if (aInitialLoad) {
      this.sort("rawHost");
    }
    if (this._view.rowCount > 0) {
      this._tree.view.selection.select(0);
    }

    if (aInitialLoad) {
      if (
        "arguments" in window &&
        window.arguments[2] &&
        window.arguments[2].filterString
      ) {
        this.setFilter(window.arguments[2].filterString);
      }
    } else if (document.getElementById("filter").value != "") {
      this.filter();
    }

    this._saveState();
  },

  _cookieEquals(aCookieA, aCookieB, aStrippedHost) {
    return (
      aCookieA.rawHost == aStrippedHost &&
      aCookieA.name == aCookieB.name &&
      aCookieA.path == aCookieB.path &&
      ChromeUtils.isOriginAttributesEqual(
        aCookieA.originAttributes,
        aCookieB.originAttributes
      )
    );
  },

  observe(aCookie, aTopic, aData) {
    if (aTopic != "cookie-changed") {
      return;
    }

    if (aCookie instanceof Ci.nsICookie) {
      var strippedHost = this._makeStrippedHost(aCookie.host);
      if (aData == "changed") {
        this._handleCookieChanged(aCookie, strippedHost);
      } else if (aData == "added") {
        this._handleCookieAdded(aCookie, strippedHost);
      }
    } else if (aData == "cleared") {
      this._hosts = {};
      this._hostOrder = [];

      var oldRowCount = this._view._rowCount;
      this._view._rowCount = 0;
      this._tree.rowCountChanged(0, -oldRowCount);
      this._view.selection.clearSelection();
    } else if (aData == "reload") {
      // first, clear any existing entries
      this.observe(aCookie, aTopic, "cleared");

      // then, reload the list
      this._populateList(false);
    }

    // We don't yet handle aData == "deleted" - it's a less common case
    // and is rather complicated as selection tracking is difficult
  },

  _handleCookieChanged(changedCookie, strippedHost) {
    var rowIndex = 0;
    var cookieItem = null;
    if (!this._view._filtered) {
      for (var i = 0; i < this._hostOrder.length; ++i) {
        // (var host in this._hosts) {
        ++rowIndex;
        var hostItem = this._hosts[this._hostOrder[i]]; // var hostItem = this._hosts[host];
        if (this._hostOrder[i] == strippedHost) {
          // host == strippedHost) {
          // Host matches, look for the cookie within this Host collection
          // and update its data
          for (var j = 0; j < hostItem.cookies.length; ++j) {
            ++rowIndex;
            var currCookie = hostItem.cookies[j];
            if (this._cookieEquals(currCookie, changedCookie, strippedHost)) {
              currCookie.value = changedCookie.value;
              currCookie.isSecure = changedCookie.isSecure;
              currCookie.isDomain = changedCookie.isDomain;
              currCookie.expires = changedCookie.expires;
              cookieItem = currCookie;
              break;
            }
          }
        } else if (hostItem.open) {
          rowIndex += hostItem.cookies.length;
        }
      }
    } else {
      // Just walk the filter list to find the item. It doesn't matter that
      // we don't update the main Host collection when we do this, because
      // when the filter is reset the Host collection is rebuilt anyway.
      for (rowIndex = 0; rowIndex < this._view._filterSet.length; ++rowIndex) {
        currCookie = this._view._filterSet[rowIndex];
        if (this._cookieEquals(currCookie, changedCookie, strippedHost)) {
          currCookie.value = changedCookie.value;
          currCookie.isSecure = changedCookie.isSecure;
          currCookie.isDomain = changedCookie.isDomain;
          currCookie.expires = changedCookie.expires;
          cookieItem = currCookie;
          break;
        }
      }
    }

    // Make sure the tree display is up to date...
    this._tree.invalidateRow(rowIndex);
    // ... and if the cookie is selected, update the displayed metadata too
    if (cookieItem != null && this._view.selection.currentIndex == rowIndex) {
      this._updateCookieData(cookieItem);
    }
  },

  _handleCookieAdded(changedCookie, strippedHost) {
    var rowCountImpact = 0;
    var addedHost = { value: 0 };
    this._addCookie(strippedHost, changedCookie, addedHost);
    if (!this._view._filtered) {
      // The Host collection for this cookie already exists, and it's not open,
      // so don't increment the rowCountImpact because the user is not going to
      // see the additional rows as they're hidden.
      if (addedHost.value || this._hosts[strippedHost].open) {
        ++rowCountImpact;
      }
    } else {
      // We're in search mode, and the cookie being added matches
      // the search condition, so add it to the list.
      var c = this._makeCookieObject(strippedHost, changedCookie);
      if (this._cookieMatchesFilter(c)) {
        this._view._filterSet.push(
          this._makeCookieObject(strippedHost, changedCookie)
        );
        ++rowCountImpact;
      }
    }
    // Now update the tree display at the end (we could/should re run the sort
    // if any to get the position correct.)
    var oldRowCount = this._rowCount;
    this._view._rowCount += rowCountImpact;
    this._tree.rowCountChanged(oldRowCount - 1, rowCountImpact);

    document.getElementById("removeAllCookies").disabled = this._view._filtered;
  },

  _view: {
    QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
    _filtered: false,
    _filterSet: [],
    _filterValue: "",
    _rowCount: 0,
    _cacheValid: 0,
    _cacheItems: [],
    get rowCount() {
      return this._rowCount;
    },

    _getItemAtIndex(aIndex) {
      if (this._filtered) {
        return this._filterSet[aIndex];
      }

      var start = 0;
      var count = 0,
        hostIndex = 0;

      var cacheIndex = Math.min(this._cacheValid, aIndex);
      if (cacheIndex > 0) {
        var cacheItem = this._cacheItems[cacheIndex];
        start = cacheItem.start;
        count = hostIndex = cacheItem.count;
      }

      for (let i = start; i < gCookiesWindow._hostOrder.length; ++i) {
        let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]];
        if (!currHost) {
          continue;
        }
        if (count == aIndex) {
          return currHost;
        }
        hostIndex = count;

        let cacheEntry = { start: i, count };
        var cacheStart = count;

        if (currHost.open) {
          if (count < aIndex && aIndex <= count + currHost.cookies.length) {
            // We are looking for an entry within this host's children,
            // enumerate them looking for the index.
            ++count;
            for (let j = 0; j < currHost.cookies.length; ++j) {
              if (count == aIndex) {
                let cookie = currHost.cookies[j];
                cookie.parentIndex = hostIndex;
                return cookie;
              }
              ++count;
            }
          } else {
            // A host entry was open, but we weren't looking for an index
            // within that host entry's children, so skip forward over the
            // entry's children. We need to add one to increment for the
            // host value too.
            count += currHost.cookies.length + 1;
          }
        } else {
          ++count;
        }

        for (let j = cacheStart; j < count; j++) {
          this._cacheItems[j] = cacheEntry;
        }
        this._cacheValid = count - 1;
      }
      return null;
    },

    _removeItemAtIndex(aIndex, aCount) {
      var removeCount = aCount === undefined ? 1 : aCount;
      if (this._filtered) {
        // remove the cookies from the unfiltered set so that they
        // don't reappear when the filter is changed. See bug 410863.
        for (let i = aIndex; i < aIndex + removeCount; ++i) {
          let item = this._filterSet[i];
          let parent = gCookiesWindow._hosts[item.rawHost];
          for (var j = 0; j < parent.cookies.length; ++j) {
            if (item == parent.cookies[j]) {
              parent.cookies.splice(j, 1);
              break;
            }
          }
        }
        this._filterSet.splice(aIndex, removeCount);
        return;
      }

      let item = this._getItemAtIndex(aIndex);
      if (!item) {
        return;
      }
      this._invalidateCache(aIndex - 1);
      if (item.container) {
        gCookiesWindow._hosts[item.rawHost] = null;
      } else {
        let parent = this._getItemAtIndex(item.parentIndex);
        for (let i = 0; i < parent.cookies.length; ++i) {
          var cookie = parent.cookies[i];
          if (
            item.rawHost == cookie.rawHost &&
            item.name == cookie.name &&
            item.path == cookie.path &&
            ChromeUtils.isOriginAttributesEqual(
              item.originAttributes,
              cookie.originAttributes
            )
          ) {
            parent.cookies.splice(i, removeCount);
          }
        }
      }
    },

    _invalidateCache(aIndex) {
      this._cacheValid = Math.min(this._cacheValid, aIndex);
    },

    getCellText(aIndex, aColumn) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return "";
        }
        if (aColumn.id == "domainCol") {
          return item.rawHost;
        }
        if (aColumn.id == "nameCol") {
          return "name" in item ? item.name : "";
        }
      } else if (aColumn.id == "domainCol") {
        return this._filterSet[aIndex].rawHost;
      } else if (aColumn.id == "nameCol") {
        return "name" in this._filterSet[aIndex]
          ? this._filterSet[aIndex].name
          : "";
      }
      return "";
    },

    _selection: null,
    get selection() {
      return this._selection;
    },
    set selection(val) {
      this._selection = val;
    },
    getRowProperties(aRow) {
      return "";
    },
    getCellProperties(aRow, aColumn) {
      return "";
    },
    getColumnProperties(aColumn) {
      return "";
    },
    isContainer(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return false;
        }
        return item.container;
      }
      return false;
    },
    isContainerOpen(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return false;
        }
        return item.open;
      }
      return false;
    },
    isContainerEmpty(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return false;
        }
        return item.cookies.length == 0;
      }
      return false;
    },
    isSeparator(aIndex) {
      return false;
    },
    isSorted(aIndex) {
      return false;
    },
    canDrop(aIndex, aOrientation) {
      return false;
    },
    drop(aIndex, aOrientation) {},
    getParentIndex(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        // If an item has no parent index (i.e. it is at the top level) this
        // function MUST return -1 otherwise we will go into an infinite loop.
        // Containers are always top level items in the cookies tree, so make
        // sure to return the appropriate value here.
        if (!item || item.container) {
          return -1;
        }
        return item.parentIndex;
      }
      return -1;
    },
    hasNextSibling(aParentIndex, aIndex) {
      if (!this._filtered) {
        // |aParentIndex| appears to be bogus, but we can get the real
        // parent index by getting the entry for |aIndex| and reading the
        // parentIndex field.
        // The index of the last item in this host collection is the
        // index of the parent + the size of the host collection, and
        // aIndex has a next sibling if it is less than this value.
        var item = this._getItemAtIndex(aIndex);
        if (item) {
          if (item.container) {
            for (var i = aIndex + 1; i < this.rowCount; ++i) {
              var subsequent = this._getItemAtIndex(i);
              if (subsequent.container) {
                return true;
              }
            }
            return false;
          }
          let parent = this._getItemAtIndex(item.parentIndex);
          if (parent && parent.container) {
            return aIndex < item.parentIndex + parent.cookies.length;
          }
        }
      }
      return aIndex < this.rowCount - 1;
    },
    hasPreviousSibling(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return false;
        }
        var parent = this._getItemAtIndex(item.parentIndex);
        if (parent && parent.container) {
          return aIndex > item.parentIndex + 1;
        }
      }
      return aIndex > 0;
    },
    getLevel(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return 0;
        }
        return item.level;
      }
      return 0;
    },
    getImageSrc(aIndex, aColumn) {},
    getProgressMode(aIndex, aColumn) {},
    getCellValue(aIndex, aColumn) {},
    setTree(aTree) {},
    toggleOpenState(aIndex) {
      if (!this._filtered) {
        var item = this._getItemAtIndex(aIndex);
        if (!item) {
          return;
        }
        this._invalidateCache(aIndex);
        var multiplier = item.open ? -1 : 1;
        var delta = multiplier * item.cookies.length;
        this._rowCount += delta;
        item.open = !item.open;
        gCookiesWindow._tree.rowCountChanged(aIndex + 1, delta);
        gCookiesWindow._tree.invalidateRow(aIndex);
      }
    },
    cycleHeader(aColumn) {},
    selectionChanged() {},
    cycleCell(aIndex, aColumn) {},
    isEditable(aIndex, aColumn) {
      return false;
    },
    setCellValue(aIndex, aColumn, aValue) {},
    setCellText(aIndex, aColumn, aValue) {},
  },

  _makeStrippedHost(aHost) {
    let formattedHost = aHost.startsWith(".")
      ? aHost.substring(1, aHost.length)
      : aHost;
    return formattedHost.startsWith("www.")
      ? formattedHost.substring(4, formattedHost.length)
      : formattedHost;
  },

  _addCookie(aStrippedHost, aCookie, aHostCount) {
    if (!(aStrippedHost in this._hosts) || !this._hosts[aStrippedHost]) {
      this._hosts[aStrippedHost] = {
        cookies: [],
        rawHost: aStrippedHost,
        level: 0,
        open: false,
        container: true,
      };
      this._hostOrder.push(aStrippedHost);
      ++aHostCount.value;
    }

    var c = this._makeCookieObject(aStrippedHost, aCookie);
    this._hosts[aStrippedHost].cookies.push(c);
  },

  _makeCookieObject(aStrippedHost, aCookie) {
    let c = {
      name: aCookie.name,
      value: aCookie.value,
      isDomain: aCookie.isDomain,
      host: aCookie.host,
      rawHost: aStrippedHost,
      path: aCookie.path,
      isSecure: aCookie.isSecure,
      expires: aCookie.expires,
      level: 1,
      container: false,
      originAttributes: aCookie.originAttributes,
    };
    return c;
  },

  _loadCookies() {
    var hostCount = { value: 0 };
    this._hosts = {};
    this._hostOrder = [];
    for (let cookie of Services.cookies.cookies) {
      var strippedHost = this._makeStrippedHost(cookie.host);
      this._addCookie(strippedHost, cookie, hostCount);
    }
    this._view._rowCount = hostCount.value;
    this._tree.rowCountChanged(0, hostCount.value);
  },

  formatExpiresString(aExpires) {
    if (aExpires) {
      var date = new Date(1000 * aExpires);
      const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
        dateStyle: "long",
        timeStyle: "long",
      });
      return dateTimeFormatter.format(date);
    }
    return this._bundle.getString("expireAtEndOfSession");
  },

  _getUserContextString(aUserContextId) {
    if (parseInt(aUserContextId, 10) == 0) {
      return this._bundle.getString("defaultUserContextLabel");
    }

    return ContextualIdentityService.getUserContextLabel(aUserContextId);
  },

  _updateCookieData(aItem) {
    var seln = this._view.selection;
    var ids = [
      "name",
      "value",
      "host",
      "path",
      "isSecure",
      "expires",
      "userContext",
    ];
    var properties;

    if (aItem && !aItem.container && seln.count > 0) {
      properties = {
        name: aItem.name,
        value: aItem.value,
        host: aItem.host,
        path: aItem.path,
        expires: this.formatExpiresString(aItem.expires),
        isDomain: aItem.isDomain
          ? this._bundle.getString("domainColon")
          : this._bundle.getString("hostColon"),
        isSecure: aItem.isSecure
          ? this._bundle.getString("forSecureOnly")
          : this._bundle.getString("forAnyConnection"),
        userContext: this._getUserContextString(
          aItem.originAttributes.userContextId
        ),
      };
      for (var i = 0; i < ids.length; ++i) {
        document.getElementById(ids[i]).disabled = false;
      }
    } else {
      var noneSelected = this._bundle.getString("noCookieSelected");
      properties = {
        name: noneSelected,
        value: noneSelected,
        host: noneSelected,
        path: noneSelected,
        expires: noneSelected,
        isSecure: noneSelected,
        userContext: noneSelected,
      };
      for (i = 0; i < ids.length; ++i) {
        document.getElementById(ids[i]).disabled = true;
      }
    }
    for (var property in properties) {
      document.getElementById(property).value = properties[property];
    }
  },

  onCookieSelected() {
    var item;
    var seln = this._tree.view.selection;
    if (!this._view._filtered) {
      item = this._view._getItemAtIndex(seln.currentIndex);
    } else {
      item = this._view._filterSet[seln.currentIndex];
    }

    this._updateCookieData(item);

    var rangeCount = seln.getRangeCount();
    var selectedCookieCount = 0;
    for (var i = 0; i < rangeCount; ++i) {
      var min = {};
      var max = {};
      seln.getRangeAt(i, min, max);
      for (var j = min.value; j <= max.value; ++j) {
        item = this._view._getItemAtIndex(j);
        if (!item) {
          continue;
        }
        if (item.container && !item.open) {
          selectedCookieCount += item.cookies.length;
        } else if (!item.container) {
          ++selectedCookieCount;
        }
      }
    }
    item = this._view._getItemAtIndex(seln.currentIndex);
    if (item && seln.count == 1 && item.container && item.open) {
      selectedCookieCount += 2;
    }

    let buttonLabel = this._bundle.getString("removeSelectedCookies");
    let removeSelectedCookies = document.getElementById(
      "removeSelectedCookies"
    );
    removeSelectedCookies.label = PluralForm.get(
      selectedCookieCount,
      buttonLabel
    ).replace("#1", selectedCookieCount);

    removeSelectedCookies.disabled = !(seln.count > 0);
    document.getElementById("removeAllCookies").disabled = this._view._filtered;
  },

  deleteCookie() {
    // Selection Notes
    // - Selection always moves to *NEXT* adjacent item unless item
    //   is last child at a given level in which case it moves to *PREVIOUS*
    //   item
    //
    // Selection Cases (Somewhat Complicated)
    //
    // 1) Single cookie selected, host has single child
    //    v cnn.com
    //    //// cnn.com ///////////// goksdjf@ ////
    //    > atwola.com
    //
    //    Before SelectedIndex: 1   Before RowCount: 3
    //    After  SelectedIndex: 0   After  RowCount: 1
    //
    // 2) Host selected, host open
    //    v goats.com ////////////////////////////
    //         goats.com             sldkkfjl
    //         goat.scom             flksj133
    //    > atwola.com
    //
    //    Before SelectedIndex: 0   Before RowCount: 4
    //    After  SelectedIndex: 0   After  RowCount: 1
    //
    // 3) Host selected, host closed
    //    > goats.com ////////////////////////////
    //    > atwola.com
    //
    //    Before SelectedIndex: 0   Before RowCount: 2
    //    After  SelectedIndex: 0   After  RowCount: 1
    //
    // 4) Single cookie selected, host has many children
    //    v goats.com
    //         goats.com             sldkkfjl
    //    //// goats.com /////////// flksjl33 ////
    //    > atwola.com
    //
    //    Before SelectedIndex: 2   Before RowCount: 4
    //    After  SelectedIndex: 1   After  RowCount: 3
    //
    // 5) Single cookie selected, host has many children
    //    v goats.com
    //    //// goats.com /////////// flksjl33 ////
    //         goats.com             sldkkfjl
    //    > atwola.com
    //
    //    Before SelectedIndex: 1   Before RowCount: 4
    //    After  SelectedIndex: 1   After  RowCount: 3
    var seln = this._view.selection;
    var tbo = this._tree;

    if (seln.count < 1) {
      return;
    }

    var nextSelected = 0;
    var rowCountImpact = 0;
    var deleteItems = [];
    if (!this._view._filtered) {
      var ci = seln.currentIndex;
      nextSelected = ci;
      var invalidateRow = -1;
      let item = this._view._getItemAtIndex(ci);
      if (item.container) {
        rowCountImpact -= (item.open ? item.cookies.length : 0) + 1;
        deleteItems = deleteItems.concat(item.cookies);
        if (!this._view.hasNextSibling(-1, ci)) {
          --nextSelected;
        }
        this._view._removeItemAtIndex(ci);
      } else {
        var parent = this._view._getItemAtIndex(item.parentIndex);
        --rowCountImpact;
        if (parent.cookies.length == 1) {
          --rowCountImpact;
          deleteItems.push(item);
          if (!this._view.hasNextSibling(-1, ci)) {
            --nextSelected;
          }
          if (!this._view.hasNextSibling(-1, item.parentIndex)) {
            --nextSelected;
          }
          this._view._removeItemAtIndex(item.parentIndex);
          invalidateRow = item.parentIndex;
        } else {
          deleteItems.push(item);
          if (!this._view.hasNextSibling(-1, ci)) {
            --nextSelected;
          }
          this._view._removeItemAtIndex(ci);
        }
      }
      this._view._rowCount += rowCountImpact;
      tbo.rowCountChanged(ci, rowCountImpact);
      if (invalidateRow != -1) {
        tbo.invalidateRow(invalidateRow);
      }
    } else {
      var rangeCount = seln.getRangeCount();
      for (var i = 0; i < rangeCount; ++i) {
        var min = {};
        var max = {};
        seln.getRangeAt(i, min, max);
        nextSelected = min.value;
        for (var j = min.value; j <= max.value; ++j) {
          deleteItems.push(this._view._getItemAtIndex(j));
          if (!this._view.hasNextSibling(-1, max.value)) {
            --nextSelected;
          }
        }
        var delta = max.value - min.value + 1;
        this._view._removeItemAtIndex(min.value, delta);
        rowCountImpact = -1 * delta;
        this._view._rowCount += rowCountImpact;
        tbo.rowCountChanged(min.value, rowCountImpact);
      }
    }

    for (let item of deleteItems) {
      Services.cookies.remove(
        item.host,
        item.name,
        item.path,
        item.originAttributes
      );
    }

    if (nextSelected < 0) {
      seln.clearSelection();
    } else {
      seln.select(nextSelected);
      this._tree.focus();
    }
  },

  deleteAllCookies() {
    Services.cookies.removeAll();
    this._tree.focus();
  },

  onCookieKeyPress(aEvent) {
    if (aEvent.keyCode == 46) {
      this.deleteCookie();
    }
  },

  _lastSortProperty: "",
  _lastSortAscending: false,
  sort(aProperty) {
    var ascending =
      aProperty == this._lastSortProperty ? !this._lastSortAscending : true;

    function sortByHost(a, b) {
      return a.toLowerCase().localeCompare(b.toLowerCase());
    }

    // Sort the Non-Filtered Host Collections
    if (aProperty == "rawHost") {
      this._hostOrder.sort(sortByHost);
      if (!ascending) {
        this._hostOrder.reverse();
      }
    }

    function sortByProperty(a, b) {
      return a[aProperty]
        .toLowerCase()
        .localeCompare(b[aProperty].toLowerCase());
    }
    for (var host in this._hosts) {
      var cookies = this._hosts[host].cookies;
      cookies.sort(sortByProperty);
      if (!ascending) {
        cookies.reverse();
      }
    }
    // Sort the Filtered List, if in Filtered mode
    if (this._view._filtered) {
      this._view._filterSet.sort(sortByProperty);
      if (!ascending) {
        this._view._filterSet.reverse();
      }
    }

    this._view._invalidateCache(0);
    this._view.selection.clearSelection();
    this._view.selection.select(0);
    this._tree.invalidate();
    this._tree.ensureRowIsVisible(0);

    this._lastSortAscending = ascending;
    this._lastSortProperty = aProperty;
  },

  clearFilter() {
    // Revert to single-select in the tree
    this._tree.setAttribute("seltype", "single");

    // Clear the Tree Display
    this._view._filtered = false;
    this._view._rowCount = 0;
    this._tree.rowCountChanged(0, -this._view._filterSet.length);
    this._view._filterSet = [];

    // Just reload the list to make sure deletions are respected
    this._loadCookies();
    this._tree.view = this._view;

    // Restore sort order
    var sortby = this._lastSortProperty;
    if (sortby == "") {
      this._lastSortAscending = false;
      this.sort("rawHost");
    } else {
      this._lastSortAscending = !this._lastSortAscending;
      this.sort(sortby);
    }

    // Restore open state
    for (var i = 0; i < this._openIndices.length; ++i) {
      this._view.toggleOpenState(this._openIndices[i]);
    }
    this._openIndices = [];

    // Restore selection
    this._view.selection.clearSelection();
    for (i = 0; i < this._lastSelectedRanges.length; ++i) {
      var range = this._lastSelectedRanges[i];
      this._view.selection.rangedSelect(range.min, range.max, true);
    }
    this._lastSelectedRanges = [];

    document.getElementById("cookiesIntro").value =
      this._bundle.getString("cookiesAll");
  },

  _cookieMatchesFilter(aCookie) {
    return (
      aCookie.rawHost.includes(this._view._filterValue) ||
      aCookie.name.includes(this._view._filterValue) ||
      aCookie.value.includes(this._view._filterValue)
    );
  },

  _filterCookies(aFilterValue) {
    this._view._filterValue = aFilterValue;
    var cookies = [];
    for (let i = 0; i < gCookiesWindow._hostOrder.length; ++i) {
      let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]];
      if (!currHost) {
        continue;
      }
      for (var j = 0; j < currHost.cookies.length; ++j) {
        var cookie = currHost.cookies[j];
        if (this._cookieMatchesFilter(cookie)) {
          cookies.push(cookie);
        }
      }
    }
    return cookies;
  },

  _lastSelectedRanges: [],
  _openIndices: [],
  _saveState() {
    // Save selection
    var seln = this._view.selection;
    this._lastSelectedRanges = [];
    var rangeCount = seln.getRangeCount();
    for (var i = 0; i < rangeCount; ++i) {
      var min = {};
      var max = {};
      seln.getRangeAt(i, min, max);
      this._lastSelectedRanges.push({ min: min.value, max: max.value });
    }

    // Save open states
    this._openIndices = [];
    for (i = 0; i < this._view.rowCount; ++i) {
      var item = this._view._getItemAtIndex(i);
      if (item && item.container && item.open) {
        this._openIndices.push(i);
      }
    }
  },

  filter() {
    var filter = document.getElementById("filter").value;
    if (filter == "") {
      gCookiesWindow.clearFilter();
      return;
    }
    var view = gCookiesWindow._view;
    view._filterSet = gCookiesWindow._filterCookies(filter);
    if (!view._filtered) {
      // Save Display Info for the Non-Filtered mode when we first
      // enter Filtered mode.
      gCookiesWindow._saveState();
      view._filtered = true;
    }
    // Move to multi-select in the tree
    gCookiesWindow._tree.setAttribute("seltype", "multiple");

    // Clear the display
    var oldCount = view._rowCount;
    view._rowCount = 0;
    gCookiesWindow._tree.rowCountChanged(0, -oldCount);
    // Set up the filtered display
    view._rowCount = view._filterSet.length;
    gCookiesWindow._tree.rowCountChanged(0, view.rowCount);

    // if the view is not empty then select the first item
    if (view.rowCount > 0) {
      view.selection.select(0);
    }

    document.getElementById("cookiesIntro").value =
      gCookiesWindow._bundle.getString("cookiesFiltered");
  },

  setFilter(aFilterString) {
    document.getElementById("filter").value = aFilterString;
    this.filter();
  },

  focusFilterBox() {
    var filter = document.getElementById("filter");
    filter.focus();
    filter.select();
  },
};
