/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */

#include "nsNameSpaceManager.h"
#include "nsGkAtoms.h"
#include "nsTreeUtils.h"
#include "nsTreeContentView.h"
#include "ChildIterator.h"
#include "nsError.h"
#include "nsXULSortService.h"
#include "nsTreeBodyFrame.h"
#include "nsTreeColumns.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/TreeContentViewBinding.h"
#include "mozilla/dom/XULTreeElement.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/dom/Document.h"

using namespace mozilla;
using namespace mozilla::dom;

// A content model view implementation for the tree.

#define ROW_FLAG_CONTAINER 0x01
#define ROW_FLAG_OPEN 0x02
#define ROW_FLAG_EMPTY 0x04
#define ROW_FLAG_SEPARATOR 0x08

class Row {
 public:
  Row(Element* aContent, int32_t aParentIndex)
      : mContent(aContent),
        mParentIndex(aParentIndex),
        mSubtreeSize(0),
        mFlags(0) {}

  ~Row() = default;

  void SetContainer(bool aContainer) {
    aContainer ? mFlags |= ROW_FLAG_CONTAINER : mFlags &= ~ROW_FLAG_CONTAINER;
  }
  bool IsContainer() { return mFlags & ROW_FLAG_CONTAINER; }

  void SetOpen(bool aOpen) {
    aOpen ? mFlags |= ROW_FLAG_OPEN : mFlags &= ~ROW_FLAG_OPEN;
  }
  bool IsOpen() { return !!(mFlags & ROW_FLAG_OPEN); }

  void SetEmpty(bool aEmpty) {
    aEmpty ? mFlags |= ROW_FLAG_EMPTY : mFlags &= ~ROW_FLAG_EMPTY;
  }
  bool IsEmpty() { return !!(mFlags & ROW_FLAG_EMPTY); }

  void SetSeparator(bool aSeparator) {
    aSeparator ? mFlags |= ROW_FLAG_SEPARATOR : mFlags &= ~ROW_FLAG_SEPARATOR;
  }
  bool IsSeparator() { return !!(mFlags & ROW_FLAG_SEPARATOR); }

  // Weak reference to a content item.
  Element* mContent;

  // The parent index of the item, set to -1 for the top level items.
  int32_t mParentIndex;

  // Subtree size for this item.
  int32_t mSubtreeSize;

 private:
  // State flags
  int8_t mFlags;
};

// We don't reference count the reference to the document
// If the document goes away first, we'll be informed and we
// can drop our reference.
// If we go away first, we'll get rid of ourselves from the
// document's observer list.

nsTreeContentView::nsTreeContentView(void)
    : mTree(nullptr), mSelection(nullptr), mDocument(nullptr) {}

nsTreeContentView::~nsTreeContentView(void) {
  // Remove ourselves from mDocument's observers.
  if (mDocument) {
    mDocument->RemoveObserver(this);
  }
}

nsresult NS_NewTreeContentView(nsITreeView** aResult) {
  *aResult = new nsTreeContentView;
  if (!*aResult) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  NS_ADDREF(*aResult);
  return NS_OK;
}

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsTreeContentView, mTree, mSelection,
                                      mBody)

NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeContentView)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeContentView)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeContentView)
  NS_INTERFACE_MAP_ENTRY(nsITreeView)
  NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
  NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITreeView)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_END

JSObject* nsTreeContentView::WrapObject(JSContext* aCx,
                                        JS::Handle<JSObject*> aGivenProto) {
  return TreeContentView_Binding::Wrap(aCx, this, aGivenProto);
}

nsISupports* nsTreeContentView::GetParentObject() { return mTree; }

NS_IMETHODIMP
nsTreeContentView::GetRowCount(int32_t* aRowCount) {
  *aRowCount = mRows.Length();

  return NS_OK;
}

NS_IMETHODIMP
nsTreeContentView::GetSelection(nsITreeSelection** aSelection) {
  NS_IF_ADDREF(*aSelection = GetSelection());

  return NS_OK;
}

bool nsTreeContentView::CanTrustTreeSelection(nsISupports* aValue) {
  // Untrusted content is only allowed to specify known-good views
  if (nsContentUtils::LegacyIsCallerChromeOrNativeCode()) {
    return true;
  }
  nsCOMPtr<nsINativeTreeSelection> nativeTreeSel = do_QueryInterface(aValue);
  return nativeTreeSel && NS_SUCCEEDED(nativeTreeSel->EnsureNative());
}

NS_IMETHODIMP
nsTreeContentView::SetSelection(nsITreeSelection* aSelection) {
  ErrorResult rv;
  SetSelection(aSelection, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::SetSelection(nsITreeSelection* aSelection,
                                     ErrorResult& aError) {
  if (aSelection && !CanTrustTreeSelection(aSelection)) {
    aError.ThrowSecurityError("Not allowed to set tree selection");
    return;
  }

  mSelection = aSelection;
}

void nsTreeContentView::GetRowProperties(int32_t aRow, nsAString& aProperties,
                                         ErrorResult& aError) {
  aProperties.Truncate();
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();
  nsIContent* realRow;
  if (row->IsSeparator()) {
    realRow = row->mContent;
  } else {
    realRow = nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  }

  if (realRow && realRow->IsElement()) {
    realRow->AsElement()->GetAttr(nsGkAtoms::properties, aProperties);
  }
}

NS_IMETHODIMP
nsTreeContentView::GetRowProperties(int32_t aIndex, nsAString& aProps) {
  ErrorResult rv;
  GetRowProperties(aIndex, aProps, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn& aColumn,
                                          nsAString& aProperties,
                                          ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();
  nsIContent* realRow =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  if (realRow) {
    Element* cell = GetCell(realRow, aColumn);
    if (cell) {
      cell->GetAttr(nsGkAtoms::properties, aProperties);
    }
  }
}

NS_IMETHODIMP
nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
                                     nsAString& aProps) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  GetCellProperties(aRow, *aCol, aProps, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::GetColumnProperties(nsTreeColumn& aColumn,
                                            nsAString& aProperties) {
  RefPtr<Element> element = aColumn.Element();

  if (element) {
    element->GetAttr(nsGkAtoms::properties, aProperties);
  }
}

NS_IMETHODIMP
nsTreeContentView::GetColumnProperties(nsTreeColumn* aCol, nsAString& aProps) {
  NS_ENSURE_ARG(aCol);

  GetColumnProperties(*aCol, aProps);
  return NS_OK;
}

bool nsTreeContentView::IsContainer(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return false;
  }

  return mRows[aRow]->IsContainer();
}

NS_IMETHODIMP
nsTreeContentView::IsContainer(int32_t aIndex, bool* _retval) {
  ErrorResult rv;
  *_retval = IsContainer(aIndex, rv);
  return rv.StealNSResult();
}

bool nsTreeContentView::IsContainerOpen(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return false;
  }

  return mRows[aRow]->IsOpen();
}

NS_IMETHODIMP
nsTreeContentView::IsContainerOpen(int32_t aIndex, bool* _retval) {
  ErrorResult rv;
  *_retval = IsContainerOpen(aIndex, rv);
  return rv.StealNSResult();
}

bool nsTreeContentView::IsContainerEmpty(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return false;
  }

  return mRows[aRow]->IsEmpty();
}

NS_IMETHODIMP
nsTreeContentView::IsContainerEmpty(int32_t aIndex, bool* _retval) {
  ErrorResult rv;
  *_retval = IsContainerEmpty(aIndex, rv);
  return rv.StealNSResult();
}

bool nsTreeContentView::IsSeparator(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return false;
  }

  return mRows[aRow]->IsSeparator();
}

NS_IMETHODIMP
nsTreeContentView::IsSeparator(int32_t aIndex, bool* _retval) {
  ErrorResult rv;
  *_retval = IsSeparator(aIndex, rv);
  return rv.StealNSResult();
}

NS_IMETHODIMP
nsTreeContentView::IsSorted(bool* _retval) {
  *_retval = IsSorted();

  return NS_OK;
}

bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation,
                                ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
  }
  return false;
}

bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation,
                                DataTransfer* aDataTransfer,
                                ErrorResult& aError) {
  return CanDrop(aRow, aOrientation, aError);
}

NS_IMETHODIMP
nsTreeContentView::CanDrop(int32_t aIndex, int32_t aOrientation,
                           DataTransfer* aDataTransfer, bool* _retval) {
  ErrorResult rv;
  *_retval = CanDrop(aIndex, aOrientation, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
                             ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
  }
}

void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
                             DataTransfer* aDataTransfer, ErrorResult& aError) {
  Drop(aRow, aOrientation, aError);
}

NS_IMETHODIMP
nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
                        DataTransfer* aDataTransfer) {
  ErrorResult rv;
  Drop(aRow, aOrientation, rv);
  return rv.StealNSResult();
}

int32_t nsTreeContentView::GetParentIndex(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return 0;
  }

  return mRows[aRow]->mParentIndex;
}

NS_IMETHODIMP
nsTreeContentView::GetParentIndex(int32_t aRowIndex, int32_t* _retval) {
  ErrorResult rv;
  *_retval = GetParentIndex(aRowIndex, rv);
  return rv.StealNSResult();
}

bool nsTreeContentView::HasNextSibling(int32_t aRow, int32_t aAfterIndex,
                                       ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return false;
  }

  // We have a next sibling if the row is not the last in the subtree.
  int32_t parentIndex = mRows[aRow]->mParentIndex;
  if (parentIndex < 0) {
    return uint32_t(aRow) < mRows.Length() - 1;
  }

  // Compute the last index in this subtree.
  int32_t lastIndex = parentIndex + (mRows[parentIndex])->mSubtreeSize;
  Row* row = mRows[lastIndex].get();
  while (row->mParentIndex != parentIndex) {
    lastIndex = row->mParentIndex;
    row = mRows[lastIndex].get();
  }

  return aRow < lastIndex;
}

NS_IMETHODIMP
nsTreeContentView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
                                  bool* _retval) {
  ErrorResult rv;
  *_retval = HasNextSibling(aRowIndex, aAfterIndex, rv);
  return rv.StealNSResult();
}

int32_t nsTreeContentView::GetLevel(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return 0;
  }

  int32_t level = 0;
  Row* row = mRows[aRow].get();
  while (row->mParentIndex >= 0) {
    level++;
    row = mRows[row->mParentIndex].get();
  }
  return level;
}

NS_IMETHODIMP
nsTreeContentView::GetLevel(int32_t aIndex, int32_t* _retval) {
  ErrorResult rv;
  *_retval = GetLevel(aIndex, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn& aColumn,
                                    nsAString& aSrc, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();

  nsIContent* realRow =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  if (realRow) {
    Element* cell = GetCell(realRow, aColumn);
    if (cell) {
      cell->GetAttr(nsGkAtoms::src, aSrc);
    }
  }
}

NS_IMETHODIMP
nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol,
                               nsAString& _retval) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  GetImageSrc(aRow, *aCol, _retval, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn& aColumn,
                                     nsAString& aValue, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();

  nsIContent* realRow =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  if (realRow) {
    Element* cell = GetCell(realRow, aColumn);
    if (cell) {
      cell->GetAttr(nsGkAtoms::value, aValue);
    }
  }
}

NS_IMETHODIMP
nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn* aCol,
                                nsAString& _retval) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  GetCellValue(aRow, *aCol, _retval, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn& aColumn,
                                    nsAString& aText, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();

  // Check for a "label" attribute - this is valid on an <treeitem>
  // with a single implied column.
  if (row->mContent->GetAttr(nsGkAtoms::label, aText) && !aText.IsEmpty()) {
    return;
  }

  if (row->mContent->IsXULElement(nsGkAtoms::treeitem)) {
    nsIContent* realRow =
        nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
    if (realRow) {
      Element* cell = GetCell(realRow, aColumn);
      if (cell) {
        cell->GetAttr(nsGkAtoms::label, aText);
      }
    }
  }
}

NS_IMETHODIMP
nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn* aCol,
                               nsAString& _retval) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  GetCellText(aRow, *aCol, _retval, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::SetTree(XULTreeElement* aTree, ErrorResult& aError) {
  aError = SetTree(aTree);
}

NS_IMETHODIMP
nsTreeContentView::SetTree(XULTreeElement* aTree) {
  ClearRows();

  mTree = aTree;

  if (aTree) {
    // Add ourselves to document's observers.
    Document* document = mTree->GetComposedDoc();
    if (document) {
      document->AddObserver(this);
      mDocument = document;
    }

    RefPtr<dom::Element> bodyElement = mTree->GetTreeBody();
    if (bodyElement) {
      mBody = std::move(bodyElement);
      int32_t index = 0;
      Serialize(mBody, -1, &index, mRows);
    }
  }

  return NS_OK;
}

void nsTreeContentView::ToggleOpenState(int32_t aRow, ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  // We don't serialize content right here, since content might be generated
  // lazily.
  Row* row = mRows[aRow].get();

  if (row->IsOpen()) {
    row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"false"_ns,
                           true);
  } else {
    row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns,
                           true);
  }
}

NS_IMETHODIMP
nsTreeContentView::ToggleOpenState(int32_t aIndex) {
  ErrorResult rv;
  ToggleOpenState(aIndex, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::CycleHeader(nsTreeColumn& aColumn,
                                    ErrorResult& aError) {
  if (!mTree) {
    return;
  }

  RefPtr<Element> column = aColumn.Element();
  nsAutoString sort;
  column->GetAttr(nsGkAtoms::sort, sort);
  if (!sort.IsEmpty()) {
    nsAutoString sortdirection;
    static Element::AttrValuesArray strings[] = {
        nsGkAtoms::ascending, nsGkAtoms::descending, nullptr};
    switch (column->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sortDirection,
                                    strings, eCaseMatters)) {
      case 0:
        sortdirection.AssignLiteral("descending");
        break;
      case 1:
        sortdirection.AssignLiteral("natural");
        break;
      default:
        sortdirection.AssignLiteral("ascending");
        break;
    }

    nsAutoString hints;
    column->GetAttr(nsGkAtoms::sorthints, hints);
    sortdirection.Append(' ');
    sortdirection += hints;

    XULWidgetSort(mTree, sort, sortdirection);
  }
}

NS_IMETHODIMP
nsTreeContentView::CycleHeader(nsTreeColumn* aCol) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  CycleHeader(*aCol, rv);
  return rv.StealNSResult();
}

NS_IMETHODIMP
nsTreeContentView::SelectionChangedXPCOM() { return NS_OK; }

NS_IMETHODIMP
nsTreeContentView::CycleCell(int32_t aRow, nsTreeColumn* aCol) { return NS_OK; }

bool nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn& aColumn,
                                   ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return false;
  }

  Row* row = mRows[aRow].get();

  nsIContent* realRow =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  if (realRow) {
    Element* cell = GetCell(realRow, aColumn);
    if (cell && cell->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
                                  nsGkAtoms::_false, eCaseMatters)) {
      return false;
    }
  }

  return true;
}

NS_IMETHODIMP
nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn* aCol, bool* _retval) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  *_retval = IsEditable(aRow, *aCol, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn& aColumn,
                                     const nsAString& aValue,
                                     ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();

  nsIContent* realRow =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  if (realRow) {
    Element* cell = GetCell(realRow, aColumn);
    if (cell) {
      cell->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true);
    }
  }
}

NS_IMETHODIMP
nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn* aCol,
                                const nsAString& aValue) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  SetCellValue(aRow, *aCol, aValue, rv);
  return rv.StealNSResult();
}

void nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn& aColumn,
                                    const nsAString& aValue,
                                    ErrorResult& aError) {
  if (!IsValidRowIndex(aRow)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  Row* row = mRows[aRow].get();

  nsIContent* realRow =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
  if (realRow) {
    Element* cell = GetCell(realRow, aColumn);
    if (cell) {
      cell->SetAttr(kNameSpaceID_None, nsGkAtoms::label, aValue, true);
    }
  }
}

NS_IMETHODIMP
nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn* aCol,
                               const nsAString& aValue) {
  NS_ENSURE_ARG(aCol);

  ErrorResult rv;
  SetCellText(aRow, *aCol, aValue, rv);
  return rv.StealNSResult();
}

Element* nsTreeContentView::GetItemAtIndex(int32_t aIndex,
                                           ErrorResult& aError) {
  if (!IsValidRowIndex(aIndex)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return nullptr;
  }

  return mRows[aIndex]->mContent;
}

int32_t nsTreeContentView::GetIndexOfItem(Element* aItem) {
  return FindContent(aItem);
}

void nsTreeContentView::AttributeChanged(dom::Element* aElement,
                                         int32_t aNameSpaceID,
                                         nsAtom* aAttribute, int32_t aModType,
                                         const nsAttrValue* aOldValue) {
  // Lots of codepaths under here that do all sorts of stuff, so be safe.
  nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);

  // Make sure this notification concerns us.
  // First check the tag to see if it's one that we care about.
  if (aElement == mTree || aElement == mBody) {
    mTree->ClearStyleAndImageCaches();
    mTree->Invalidate();
  }

  // We don't consider non-XUL nodes.
  nsIContent* parent = nullptr;
  if (!aElement->IsXULElement() ||
      ((parent = aElement->GetParent()) && !parent->IsXULElement())) {
    return;
  }
  if (!aElement->IsAnyOfXULElements(nsGkAtoms::treecol, nsGkAtoms::treeitem,
                                    nsGkAtoms::treeseparator,
                                    nsGkAtoms::treerow, nsGkAtoms::treecell)) {
    return;
  }

  // If we have a legal tag, go up to the tree/select and make sure
  // that it's ours.

  for (nsIContent* element = aElement; element != mBody;
       element = element->GetParent()) {
    if (!element) {
      return;  // this is not for us
    }
    if (element->IsXULElement(nsGkAtoms::tree)) {
      return;  // this is not for us
    }
  }

  // Handle changes of the hidden attribute.
  if (aAttribute == nsGkAtoms::hidden &&
      aElement->IsAnyOfXULElements(nsGkAtoms::treeitem,
                                   nsGkAtoms::treeseparator)) {
    bool hidden = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
                                        nsGkAtoms::_true, eCaseMatters);

    int32_t index = FindContent(aElement);
    if (hidden && index >= 0) {
      // Hide this row along with its children.
      int32_t count = RemoveRow(index);
      if (mTree) {
        mTree->RowCountChanged(index, -count);
      }
    } else if (!hidden && index < 0) {
      // Show this row along with its children.
      nsCOMPtr<nsIContent> parent = aElement->GetParent();
      if (parent) {
        InsertRowFor(parent, aElement);
      }
    }

    return;
  }

  if (aElement->IsXULElement(nsGkAtoms::treecol)) {
    if (aAttribute == nsGkAtoms::properties) {
      if (mTree) {
        RefPtr<nsTreeColumns> cols = mTree->GetColumns();
        if (cols) {
          RefPtr<nsTreeColumn> col = cols->GetColumnFor(aElement);
          mTree->InvalidateColumn(col);
        }
      }
    }
  } else if (aElement->IsXULElement(nsGkAtoms::treeitem)) {
    int32_t index = FindContent(aElement);
    if (index >= 0) {
      Row* row = mRows[index].get();
      if (aAttribute == nsGkAtoms::container) {
        bool isContainer =
            aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container,
                                  nsGkAtoms::_true, eCaseMatters);
        row->SetContainer(isContainer);
        if (mTree) {
          mTree->InvalidateRow(index);
        }
      } else if (aAttribute == nsGkAtoms::open) {
        bool isOpen = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open,
                                            nsGkAtoms::_true, eCaseMatters);
        bool wasOpen = row->IsOpen();
        if (!isOpen && wasOpen) {
          CloseContainer(index);
        } else if (isOpen && !wasOpen) {
          OpenContainer(index);
        }
      } else if (aAttribute == nsGkAtoms::empty) {
        bool isEmpty =
            aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty,
                                  nsGkAtoms::_true, eCaseMatters);
        row->SetEmpty(isEmpty);
        if (mTree) {
          mTree->InvalidateRow(index);
        }
      }
    }
  } else if (aElement->IsXULElement(nsGkAtoms::treeseparator)) {
    int32_t index = FindContent(aElement);
    if (index >= 0) {
      if (aAttribute == nsGkAtoms::properties && mTree) {
        mTree->InvalidateRow(index);
      }
    }
  } else if (aElement->IsXULElement(nsGkAtoms::treerow)) {
    if (aAttribute == nsGkAtoms::properties) {
      nsCOMPtr<nsIContent> parent = aElement->GetParent();
      if (parent) {
        int32_t index = FindContent(parent);
        if (index >= 0 && mTree) {
          mTree->InvalidateRow(index);
        }
      }
    }
  } else if (aElement->IsXULElement(nsGkAtoms::treecell)) {
    if (aAttribute == nsGkAtoms::properties || aAttribute == nsGkAtoms::mode ||
        aAttribute == nsGkAtoms::src || aAttribute == nsGkAtoms::value ||
        aAttribute == nsGkAtoms::label) {
      nsIContent* parent = aElement->GetParent();
      if (parent) {
        nsCOMPtr<nsIContent> grandParent = parent->GetParent();
        if (grandParent && grandParent->IsXULElement()) {
          int32_t index = FindContent(grandParent);
          if (index >= 0 && mTree) {
            // XXX Should we make an effort to invalidate only cell ?
            mTree->InvalidateRow(index);
          }
        }
      }
    }
  }
}

void nsTreeContentView::ContentAppended(nsIContent* aFirstNewContent) {
  for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
    // Our contentinserted doesn't use the index
    ContentInserted(cur);
  }
}

void nsTreeContentView::ContentInserted(nsIContent* aChild) {
  NS_ASSERTION(aChild, "null ptr");
  nsIContent* container = aChild->GetParent();

  // Make sure this notification concerns us.
  // First check the tag to see if it's one that we care about.

  // Don't allow non-XUL nodes.
  if (!aChild->IsXULElement() || !container->IsXULElement()) {
    return;
  }

  if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator,
                                  nsGkAtoms::treechildren, nsGkAtoms::treerow,
                                  nsGkAtoms::treecell)) {
    return;
  }

  // If we have a legal tag, go up to the tree/select and make sure
  // that it's ours.

  for (nsIContent* element = container; element != mBody;
       element = element->GetParent()) {
    if (!element) {
      return;  // this is not for us
    }
    if (element->IsXULElement(nsGkAtoms::tree)) {
      return;  // this is not for us
    }
  }

  // Lots of codepaths under here that do all sorts of stuff, so be safe.
  nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);

  if (aChild->IsXULElement(nsGkAtoms::treechildren)) {
    int32_t index = FindContent(container);
    if (index >= 0) {
      Row* row = mRows[index].get();
      row->SetEmpty(false);
      if (mTree) {
        mTree->InvalidateRow(index);
      }
      if (row->IsContainer() && row->IsOpen()) {
        int32_t count = EnsureSubtree(index);
        if (mTree) {
          mTree->RowCountChanged(index + 1, count);
        }
      }
    }
  } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem,
                                        nsGkAtoms::treeseparator)) {
    InsertRowFor(container, aChild);
  } else if (aChild->IsXULElement(nsGkAtoms::treerow)) {
    int32_t index = FindContent(container);
    if (index >= 0 && mTree) {
      mTree->InvalidateRow(index);
    }
  } else if (aChild->IsXULElement(nsGkAtoms::treecell)) {
    nsCOMPtr<nsIContent> parent = container->GetParent();
    if (parent) {
      int32_t index = FindContent(parent);
      if (index >= 0 && mTree) {
        mTree->InvalidateRow(index);
      }
    }
  }
}

void nsTreeContentView::ContentWillBeRemoved(nsIContent* aChild,
                                             const BatchRemovalState*) {
  NS_ASSERTION(aChild, "null ptr");

  nsIContent* container = aChild->GetParent();
  // Make sure this notification concerns us.
  // First check the tag to see if it's one that we care about.

  // We don't consider non-XUL nodes.
  if (!aChild->IsXULElement() || !container->IsXULElement()) {
    return;
  }

  if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator,
                                  nsGkAtoms::treechildren, nsGkAtoms::treerow,
                                  nsGkAtoms::treecell)) {
    return;
  }

  // If we have a legal tag, go up to the tree/select and make sure
  // that it's ours.

  for (nsIContent* element = container; element != mBody;
       element = element->GetParent()) {
    if (!element) {
      return;  // this is not for us
    }
    if (element->IsXULElement(nsGkAtoms::tree)) {
      return;  // this is not for us
    }
  }

  // Lots of codepaths under here that do all sorts of stuff, so be safe.
  nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);

  if (aChild->IsXULElement(nsGkAtoms::treechildren)) {
    int32_t index = FindContent(container);
    if (index >= 0) {
      Row* row = mRows[index].get();
      row->SetEmpty(true);
      int32_t count = RemoveSubtree(index);
      // Invalidate also the row to update twisty.
      if (mTree) {
        mTree->InvalidateRow(index);
        mTree->RowCountChanged(index + 1, -count);
      }
    }
  } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem,
                                        nsGkAtoms::treeseparator)) {
    int32_t index = FindContent(aChild);
    if (index >= 0) {
      int32_t count = RemoveRow(index);
      if (mTree) {
        mTree->RowCountChanged(index, -count);
      }
    }
  } else if (aChild->IsXULElement(nsGkAtoms::treerow)) {
    int32_t index = FindContent(container);
    if (index >= 0 && mTree) {
      mTree->InvalidateRow(index);
    }
  } else if (aChild->IsXULElement(nsGkAtoms::treecell)) {
    nsCOMPtr<nsIContent> parent = container->GetParent();
    if (parent) {
      int32_t index = FindContent(parent);
      if (index >= 0 && mTree) {
        mTree->InvalidateRow(index);
      }
    }
  }
}

void nsTreeContentView::NodeWillBeDestroyed(nsINode* aNode) {
  // XXXbz do we need this strong ref?  Do we drop refs to self in ClearRows?
  nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
  ClearRows();
}

// Recursively serialize content, starting with aContent.
void nsTreeContentView::Serialize(nsIContent* aContent, int32_t aParentIndex,
                                  int32_t* aIndex,
                                  nsTArray<UniquePtr<Row>>& aRows) {
  // Don't allow non-XUL nodes.
  if (!aContent->IsXULElement()) {
    return;
  }

  dom::FlattenedChildIterator iter(aContent);
  for (nsIContent* content = iter.GetNextChild(); content;
       content = iter.GetNextChild()) {
    int32_t count = aRows.Length();

    if (content->IsXULElement(nsGkAtoms::treeitem)) {
      SerializeItem(content->AsElement(), aParentIndex, aIndex, aRows);
    } else if (content->IsXULElement(nsGkAtoms::treeseparator)) {
      SerializeSeparator(content->AsElement(), aParentIndex, aIndex, aRows);
    }

    *aIndex += aRows.Length() - count;
  }
}

void nsTreeContentView::SerializeItem(Element* aContent, int32_t aParentIndex,
                                      int32_t* aIndex,
                                      nsTArray<UniquePtr<Row>>& aRows) {
  if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
                            nsGkAtoms::_true, eCaseMatters)) {
    return;
  }

  aRows.AppendElement(MakeUnique<Row>(aContent, aParentIndex));
  Row* row = aRows.LastElement().get();

  if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container,
                            nsGkAtoms::_true, eCaseMatters)) {
    row->SetContainer(true);
    if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open,
                              nsGkAtoms::_true, eCaseMatters)) {
      row->SetOpen(true);
      nsIContent* child =
          nsTreeUtils::GetImmediateChild(aContent, nsGkAtoms::treechildren);
      if (child && child->IsXULElement()) {
        // Now, recursively serialize our child.
        int32_t count = aRows.Length();
        int32_t index = 0;
        Serialize(child, aParentIndex + *aIndex + 1, &index, aRows);
        row->mSubtreeSize += aRows.Length() - count;
      } else {
        row->SetEmpty(true);
      }
    } else if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty,
                                     nsGkAtoms::_true, eCaseMatters)) {
      row->SetEmpty(true);
    }
  }
}

void nsTreeContentView::SerializeSeparator(Element* aContent,
                                           int32_t aParentIndex,
                                           int32_t* aIndex,
                                           nsTArray<UniquePtr<Row>>& aRows) {
  if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
                            nsGkAtoms::_true, eCaseMatters)) {
    return;
  }

  auto row = MakeUnique<Row>(aContent, aParentIndex);
  row->SetSeparator(true);
  aRows.AppendElement(std::move(row));
}

void nsTreeContentView::GetIndexInSubtree(nsIContent* aContainer,
                                          nsIContent* aContent,
                                          int32_t* aIndex) {
  if (!aContainer->IsXULElement()) {
    return;
  }

  for (nsIContent* content = aContainer->GetFirstChild(); content;
       content = content->GetNextSibling()) {
    if (content == aContent) {
      break;
    }

    if (content->IsXULElement(nsGkAtoms::treeitem)) {
      if (!content->AsElement()->AttrValueIs(kNameSpaceID_None,
                                             nsGkAtoms::hidden,
                                             nsGkAtoms::_true, eCaseMatters)) {
        (*aIndex)++;
        if (content->AsElement()->AttrValueIs(kNameSpaceID_None,
                                              nsGkAtoms::container,
                                              nsGkAtoms::_true, eCaseMatters) &&
            content->AsElement()->AttrValueIs(kNameSpaceID_None,
                                              nsGkAtoms::open, nsGkAtoms::_true,
                                              eCaseMatters)) {
          nsIContent* child =
              nsTreeUtils::GetImmediateChild(content, nsGkAtoms::treechildren);
          if (child && child->IsXULElement()) {
            GetIndexInSubtree(child, aContent, aIndex);
          }
        }
      }
    } else if (content->IsXULElement(nsGkAtoms::treeseparator)) {
      if (!content->AsElement()->AttrValueIs(kNameSpaceID_None,
                                             nsGkAtoms::hidden,
                                             nsGkAtoms::_true, eCaseMatters)) {
        (*aIndex)++;
      }
    }
  }
}

int32_t nsTreeContentView::EnsureSubtree(int32_t aIndex) {
  Row* row = mRows[aIndex].get();

  nsIContent* child;
  child =
      nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treechildren);
  if (!child || !child->IsXULElement()) {
    return 0;
  }

  AutoTArray<UniquePtr<Row>, 8> rows;
  int32_t index = 0;
  Serialize(child, aIndex, &index, rows);
  // Insert |rows| into |mRows| at position |aIndex|, by first creating empty
  // UniquePtr entries and then Move'ing |rows|'s entries into them. (Note
  // that we can't simply use InsertElementsAt with an array argument, since
  // the destination can't steal ownership from its const source argument.)
  UniquePtr<Row>* newRows = mRows.InsertElementsAt(aIndex + 1, rows.Length());
  for (nsTArray<Row>::index_type i = 0; i < rows.Length(); i++) {
    newRows[i] = std::move(rows[i]);
  }
  int32_t count = rows.Length();

  row->mSubtreeSize += count;
  UpdateSubtreeSizes(row->mParentIndex, count);

  // Update parent indexes, but skip newly added rows.
  // They already have correct values.
  UpdateParentIndexes(aIndex, count + 1, count);

  return count;
}

int32_t nsTreeContentView::RemoveSubtree(int32_t aIndex) {
  Row* row = mRows[aIndex].get();
  int32_t count = row->mSubtreeSize;

  mRows.RemoveElementsAt(aIndex + 1, count);

  row->mSubtreeSize -= count;
  UpdateSubtreeSizes(row->mParentIndex, -count);

  UpdateParentIndexes(aIndex, 0, -count);

  return count;
}

void nsTreeContentView::InsertRowFor(nsIContent* aParent, nsIContent* aChild) {
  int32_t grandParentIndex = -1;
  bool insertRow = false;

  nsCOMPtr<nsIContent> grandParent = aParent->GetParent();

  if (grandParent->IsXULElement(nsGkAtoms::tree)) {
    // Allow insertion to the outermost container.
    insertRow = true;
  } else {
    // Test insertion to an inner container.

    // First try to find this parent in our array of rows, if we find one
    // we can be sure that all other parents are open too.
    grandParentIndex = FindContent(grandParent);
    if (grandParentIndex >= 0) {
      // Got it, now test if it is open.
      if (mRows[grandParentIndex]->IsOpen()) {
        insertRow = true;
      }
    }
  }

  if (insertRow) {
    int32_t index = 0;
    GetIndexInSubtree(aParent, aChild, &index);

    int32_t count = InsertRow(grandParentIndex, index, aChild);
    if (mTree) {
      mTree->RowCountChanged(grandParentIndex + index + 1, count);
    }
  }
}

int32_t nsTreeContentView::InsertRow(int32_t aParentIndex, int32_t aIndex,
                                     nsIContent* aContent) {
  AutoTArray<UniquePtr<Row>, 8> rows;
  if (aContent->IsXULElement(nsGkAtoms::treeitem)) {
    SerializeItem(aContent->AsElement(), aParentIndex, &aIndex, rows);
  } else if (aContent->IsXULElement(nsGkAtoms::treeseparator)) {
    SerializeSeparator(aContent->AsElement(), aParentIndex, &aIndex, rows);
  }

  // We can't use InsertElementsAt since the destination can't steal
  // ownership from its const source argument.
  int32_t count = rows.Length();
  for (nsTArray<Row>::index_type i = 0; i < size_t(count); i++) {
    mRows.InsertElementAt(aParentIndex + aIndex + i + 1, std::move(rows[i]));
  }

  UpdateSubtreeSizes(aParentIndex, count);

  // Update parent indexes, but skip added rows.
  // They already have correct values.
  UpdateParentIndexes(aParentIndex + aIndex, count + 1, count);

  return count;
}

int32_t nsTreeContentView::RemoveRow(int32_t aIndex) {
  Row* row = mRows[aIndex].get();
  int32_t count = row->mSubtreeSize + 1;
  int32_t parentIndex = row->mParentIndex;

  mRows.RemoveElementsAt(aIndex, count);

  UpdateSubtreeSizes(parentIndex, -count);

  UpdateParentIndexes(aIndex, 0, -count);

  return count;
}

void nsTreeContentView::ClearRows() {
  mRows.Clear();
  mBody = nullptr;
  // Remove ourselves from mDocument's observers.
  if (mDocument) {
    mDocument->RemoveObserver(this);
    mDocument = nullptr;
  }
}

void nsTreeContentView::OpenContainer(int32_t aIndex) {
  Row* row = mRows[aIndex].get();
  row->SetOpen(true);

  int32_t count = EnsureSubtree(aIndex);
  if (mTree) {
    mTree->InvalidateRow(aIndex);
    mTree->RowCountChanged(aIndex + 1, count);
  }
}

void nsTreeContentView::CloseContainer(int32_t aIndex) {
  Row* row = mRows[aIndex].get();
  row->SetOpen(false);

  int32_t count = RemoveSubtree(aIndex);
  if (mTree) {
    mTree->InvalidateRow(aIndex);
    mTree->RowCountChanged(aIndex + 1, -count);
  }
}

int32_t nsTreeContentView::FindContent(nsIContent* aContent) {
  for (uint32_t i = 0; i < mRows.Length(); i++) {
    if (mRows[i]->mContent == aContent) {
      return i;
    }
  }

  return -1;
}

void nsTreeContentView::UpdateSubtreeSizes(int32_t aParentIndex,
                                           int32_t count) {
  while (aParentIndex >= 0) {
    Row* row = mRows[aParentIndex].get();
    row->mSubtreeSize += count;
    aParentIndex = row->mParentIndex;
  }
}

void nsTreeContentView::UpdateParentIndexes(int32_t aIndex, int32_t aSkip,
                                            int32_t aCount) {
  int32_t count = mRows.Length();
  for (int32_t i = aIndex + aSkip; i < count; i++) {
    Row* row = mRows[i].get();
    if (row->mParentIndex > aIndex) {
      row->mParentIndex += aCount;
    }
  }
}

Element* nsTreeContentView::GetCell(nsIContent* aContainer,
                                    nsTreeColumn& aCol) {
  int32_t colIndex(aCol.GetIndex());

  // Traverse through cells, try to find the cell by index in a row.
  Element* result = nullptr;
  int32_t j = 0;
  dom::FlattenedChildIterator iter(aContainer);
  for (nsIContent* cell = iter.GetNextChild(); cell;
       cell = iter.GetNextChild()) {
    if (cell->IsXULElement(nsGkAtoms::treecell)) {
      if (j == colIndex) {
        result = cell->AsElement();
        break;
      }
      j++;
    }
  }

  return result;
}

bool nsTreeContentView::IsValidRowIndex(int32_t aRowIndex) {
  return aRowIndex >= 0 && aRowIndex < int32_t(mRows.Length());
}
