/* 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 "mozilla/dom/XULTreeElement.h"

#include "ChildIterator.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/DOMRect.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ToJSValue.h"
#include "mozilla/dom/XULTreeElementBinding.h"
#include "nsCOMPtr.h"
#include "nsError.h"
#include "nsITreeSelection.h"
#include "nsScrollbarFrame.h"
#include "nsTreeBodyFrame.h"
#include "nsTreeContentView.h"

namespace mozilla::dom {

NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULTreeElement, nsXULElement)
NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeElement, nsXULElement, mView)

JSObject* XULTreeElement::WrapNode(JSContext* aCx,
                                   JS::Handle<JSObject*> aGivenProto) {
  return XULTreeElement_Binding::Wrap(aCx, this, aGivenProto);
}

void XULTreeElement::UnbindFromTree(UnbindContext& aContext) {
  // Drop the view's ref to us.
  if (mView) {
    nsCOMPtr<nsITreeSelection> sel;
    mView->GetSelection(getter_AddRefs(sel));
    if (sel) {
      sel->SetTree(nullptr);
    }
    mView->SetTree(nullptr);  // Break the circular ref between the view and us.
  }
  mView = nullptr;

  nsXULElement::UnbindFromTree(aContext);
}

void XULTreeElement::DestroyContent() {
  // Drop the view's ref to us.
  if (mView) {
    nsCOMPtr<nsITreeSelection> sel;
    mView->GetSelection(getter_AddRefs(sel));
    if (sel) {
      sel->SetTree(nullptr);
    }
    mView->SetTree(nullptr);  // Break the circular ref between the view and us.
  }
  mView = nullptr;

  nsXULElement::DestroyContent();
}

static nsIContent* FindBodyElement(nsIContent* aParent) {
  mozilla::dom::FlattenedChildIterator iter(aParent);
  for (nsIContent* content = iter.GetNextChild(); content;
       content = iter.GetNextChild()) {
    mozilla::dom::NodeInfo* ni = content->NodeInfo();
    if (ni->Equals(nsGkAtoms::treechildren, kNameSpaceID_XUL)) {
      return content;
    } else if (ni->Equals(nsGkAtoms::tree, kNameSpaceID_XUL)) {
      // There are nesting tree elements. Only the innermost should
      // find the treechilren.
      return nullptr;
    } else if (content->IsElement() &&
               !ni->Equals(nsGkAtoms::_template, kNameSpaceID_XUL)) {
      nsIContent* result = FindBodyElement(content);
      if (result) return result;
    }
  }

  return nullptr;
}

nsTreeBodyFrame* XULTreeElement::GetTreeBodyFrame(FlushType aFlushType) {
  MOZ_ASSERT(aFlushType == FlushType::Frames ||
             aFlushType == FlushType::Layout || aFlushType == FlushType::None);
  nsCOMPtr<nsIContent> kungFuDeathGrip = this;  // keep a reference

  // Make sure our frames are up to date, and layout as needed.  We
  // have to do this before checking for our cached mTreeBody, since
  // it might go away on style flush, and in any case if aFlushLayout
  // is true we need to make sure to flush no matter what.
  if (aFlushType != FlushType::None) {
    if (RefPtr<Document> doc = GetComposedDoc()) {
      doc->FlushPendingNotifications(aFlushType);
    }
  }

  if (mTreeBody) {
    // Have one cached already.
    return mTreeBody;
  }

  if (nsCOMPtr<nsIContent> tree = FindBodyElement(this)) {
    mTreeBody = do_QueryFrame(tree->GetPrimaryFrame());
  }

  return mTreeBody;
}

already_AddRefed<nsITreeView> XULTreeElement::GetView(FlushType aFlushType) {
  if (!mTreeBody) {
    if (!GetTreeBodyFrame(aFlushType)) {
      return nullptr;
    }

    if (mView) {
      nsCOMPtr<nsITreeView> view;
      // Our new frame needs to initialise itself
      mTreeBody->GetView(getter_AddRefs(view));
      return view.forget();
    }
  }
  if (!mView) {
    // No tree builder, create a tree content view.
    if (NS_FAILED(NS_NewTreeContentView(getter_AddRefs(mView)))) {
      return nullptr;
    }

    // Initialise the frame and view
    mTreeBody->SetView(mView);
  }

  return do_AddRef(mView);
}

void XULTreeElement::SetView(nsITreeView* aView, CallerType aCallerType,
                             ErrorResult& aRv) {
  if (aCallerType != CallerType::System) {
    // Don't trust views coming from random places.
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return;
  }

  mView = aView;
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->SetView(aView);
  }
}

bool XULTreeElement::Focused() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    return body->GetFocused();
  }
  return false;
}

void XULTreeElement::SetFocused(bool aFocused) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->SetFocused(aFocused);
  }
}

already_AddRefed<Element> XULTreeElement::GetTreeBody() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    nsCOMPtr<Element> element;
    body->GetTreeBody(getter_AddRefs(element));
    return element.forget();
  }

  return nullptr;
}

already_AddRefed<nsTreeColumns> XULTreeElement::GetColumns(
    FlushType aFlushType) {
  if (nsTreeBodyFrame* body = GetTreeBodyFrame(aFlushType)) {
    return body->Columns();
  }
  return nullptr;
}

int32_t XULTreeElement::RowHeight() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    return body->RowHeight();
  }
  return 0;
}

int32_t XULTreeElement::RowWidth() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    return body->RowWidth();
  }
  return 0;
}

int32_t XULTreeElement::GetFirstVisibleRow() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    return body->FirstVisibleRow();
  }
  return 0;
}

int32_t XULTreeElement::GetLastVisibleRow() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    return body->LastVisibleRow();
  }
  return 0;
}

int32_t XULTreeElement::GetPageLength() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    return body->PageLength();
  }
  return 0;
}

void XULTreeElement::EnsureRowIsVisible(int32_t aRow) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->EnsureRowIsVisible(aRow);
  }
}

void XULTreeElement::EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol,
                                         ErrorResult& aRv) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    nsresult rv = body->EnsureCellIsVisible(aRow, aCol);
    if (NS_FAILED(rv)) {
      aRv.Throw(rv);
    }
  }
}

void XULTreeElement::ScrollToRow(int32_t aRow) {
  nsTreeBodyFrame* body = GetTreeBodyFrame(FlushType::Layout);
  if (!body) {
    return;
  }

  body->ScrollToRow(aRow);
}

void XULTreeElement::ScrollByLines(int32_t aNumLines) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (!body) {
    return;
  }
  body->ScrollByLines(aNumLines);
}

void XULTreeElement::ScrollByPages(int32_t aNumPages) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->ScrollByPages(aNumPages);
  }
}

void XULTreeElement::Invalidate() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->Invalidate();
  }
}

void XULTreeElement::InvalidateColumn(nsTreeColumn* aCol) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->InvalidateColumn(aCol);
  }
}

void XULTreeElement::InvalidateRow(int32_t aIndex) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->InvalidateRow(aIndex);
  }
}

void XULTreeElement::InvalidateCell(int32_t aRow, nsTreeColumn* aCol) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->InvalidateCell(aRow, aCol);
  }
}

void XULTreeElement::InvalidateRange(int32_t aStart, int32_t aEnd) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->InvalidateRange(aStart, aEnd);
  }
}

int32_t XULTreeElement::GetRowAt(int32_t x, int32_t y) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (!body) {
    return 0;
  }
  return body->GetRowAt(x, y);
}

void XULTreeElement::GetCellAt(int32_t aX, int32_t aY, TreeCellInfo& aRetVal,
                               ErrorResult& aRv) {
  aRetVal.mRow = 0;
  aRetVal.mCol = nullptr;

  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    nsAutoCString element;
    body->GetCellAt(aX, aY, &aRetVal.mRow, getter_AddRefs(aRetVal.mCol),
                    element);
    CopyUTF8toUTF16(element, aRetVal.mChildElt);
  }
}

nsIntRect XULTreeElement::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol,
                                               const nsAString& aElement,
                                               nsresult& rv) {
  rv = NS_OK;
  nsIntRect rect;

  nsTreeBodyFrame* body = GetTreeBodyFrame();
  NS_ConvertUTF16toUTF8 element(aElement);
  if (body) {
    rv = body->GetCoordsForCellItem(aRow, aCol, element, &rect.x, &rect.y,
                                    &rect.width, &rect.height);
  }

  return rect;
}

already_AddRefed<DOMRect> XULTreeElement::GetCoordsForCellItem(
    int32_t aRow, nsTreeColumn& aCol, const nsAString& aElement,
    ErrorResult& aRv) {
  nsresult rv;
  nsIntRect rect = GetCoordsForCellItem(aRow, &aCol, aElement, rv);
  aRv = rv;

  RefPtr<DOMRect> domRect = new DOMRect(ToSupports(OwnerDoc()), rect.x, rect.y,
                                        rect.width, rect.height);
  return domRect.forget();
}

bool XULTreeElement::IsCellCropped(int32_t aRow, nsTreeColumn* aCol,
                                   ErrorResult& aRv) {
  bool cropped = false;

  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    aRv = body->IsCellCropped(aRow, aCol, &cropped);
  }

  return cropped;
}

void XULTreeElement::RowCountChanged(int32_t aIndex, int32_t aDelta) {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->RowCountChanged(aIndex, aDelta);
  }
}

void XULTreeElement::BeginUpdateBatch() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->BeginUpdateBatch();
  }
}

void XULTreeElement::EndUpdateBatch() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->EndUpdateBatch();
  }
}

void XULTreeElement::ClearStyleAndImageCaches() {
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->ClearStyleAndImageCaches();
  }
}

void XULTreeElement::RemoveImageCacheEntry(int32_t aRowIndex,
                                           nsTreeColumn& aCol,
                                           ErrorResult& aRv) {
  if (NS_WARN_IF(aRowIndex < 0)) {
    aRv.Throw(NS_ERROR_INVALID_ARG);
    return;
  }
  nsTreeBodyFrame* body = GetTreeBodyFrame();
  if (body) {
    body->RemoveImageCacheEntry(aRowIndex, &aCol);
  }
}

nsScrollbarFrame* FindScrollbar(const XULTreeElement* aTree) {
  auto* shadow = aTree->GetShadowRoot();
  if (!shadow) {
    return nullptr;
  }
  for (nsINode* cur = shadow; cur; cur = cur->GetNextNode(shadow)) {
    if (cur->IsXULElement(nsGkAtoms::scrollbar)) {
      return do_QueryFrame(cur->AsElement()->GetPrimaryFrame());
    }
  }
  return nullptr;
}

int32_t XULTreeElement::ScrollbarPosition() const {
  if (auto* sb = FindScrollbar(this)) {
    return sb->GetCurPos();
  }
  return 0;
}

int32_t XULTreeElement::ScrollbarMaxPosition() const {
  if (auto* sb = FindScrollbar(this)) {
    return sb->GetMaxPos();
  }
  return 0;
}

}  // namespace mozilla::dom
