/* -*- 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/. */

/*
 * Storage of the children and attributes of a DOM node; storage for
 * the two is unified to minimize footprint.
 */

#include "AttrArray.h"

#include "mozilla/AttributeStyles.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/ServoBindings.h"

#include "nsString.h"
#include "nsUnicharUtils.h"
#include "nsContentUtils.h"  // nsAutoScriptBlocker

using mozilla::CheckedUint32;

AttrArray::Impl::~Impl() {
  for (InternalAttr& attr : Attrs()) {
    attr.~InternalAttr();
  }
  if (auto* decl = GetMappedDeclarationBlock()) {
    Servo_DeclarationBlock_Release(decl);
    mMappedAttributeBits = 0;
  }
}

void AttrArray::SetMappedDeclarationBlock(
    already_AddRefed<mozilla::StyleLockedDeclarationBlock> aBlock) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mImpl);
  MOZ_ASSERT(IsPendingMappedAttributeEvaluation());
  if (auto* decl = GetMappedDeclarationBlock()) {
    Servo_DeclarationBlock_Release(decl);
  }
  mImpl->mMappedAttributeBits = reinterpret_cast<uintptr_t>(aBlock.take());
  MOZ_ASSERT(!IsPendingMappedAttributeEvaluation());
}

const nsAttrValue* AttrArray::GetAttr(const nsAtom* aLocalName) const {
  NS_ASSERTION(aLocalName, "Must have attr name");
  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(aLocalName)) {
      return &attr.mValue;
    }
  }
  return nullptr;
}

const nsAttrValue* AttrArray::GetAttr(const nsAtom* aLocalName,
                                      int32_t aNamespaceID) const {
  NS_ASSERTION(aLocalName, "Must have attr name");
  NS_ASSERTION(aNamespaceID != kNameSpaceID_Unknown, "Must have namespace");
  if (aNamespaceID == kNameSpaceID_None) {
    // This should be the common case so lets use the optimized loop
    return GetAttr(aLocalName);
  }
  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(aLocalName, aNamespaceID)) {
      return &attr.mValue;
    }
  }
  return nullptr;
}

const nsAttrValue* AttrArray::GetAttr(const nsAString& aLocalName) const {
  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(aLocalName)) {
      return &attr.mValue;
    }
  }
  return nullptr;
}

const nsAttrValue* AttrArray::GetAttr(const nsAString& aName,
                                      nsCaseTreatment aCaseSensitive) const {
  // Check whether someone is being silly and passing non-lowercase
  // attr names.
  if (aCaseSensitive == eIgnoreCase &&
      nsContentUtils::StringContainsASCIIUpper(aName)) {
    // Try again with a lowercased name, but make sure we can't reenter this
    // block by passing eCaseSensitive for aCaseSensitive.
    nsAutoString lowercase;
    nsContentUtils::ASCIIToLower(aName, lowercase);
    return GetAttr(lowercase, eCaseMatters);
  }

  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.QualifiedNameEquals(aName)) {
      return &attr.mValue;
    }
  }

  return nullptr;
}

const nsAttrValue* AttrArray::AttrAt(uint32_t aPos) const {
  NS_ASSERTION(aPos < AttrCount(), "out-of-bounds access in AttrArray");
  return &mImpl->Attrs()[aPos].mValue;
}

template <typename Name>
inline nsresult AttrArray::AddNewAttribute(Name* aName, nsAttrValue& aValue) {
  MOZ_ASSERT(!mImpl || mImpl->mCapacity >= mImpl->mAttrCount);
  if (!mImpl || mImpl->mCapacity == mImpl->mAttrCount) {
    if (!GrowBy(1)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }

  InternalAttr& attr = mImpl->mBuffer[mImpl->mAttrCount++];
  new (&attr.mName) nsAttrName(aName);
  new (&attr.mValue) nsAttrValue();
  attr.mValue.SwapValueWith(aValue);
  return NS_OK;
}

nsresult AttrArray::SetAndSwapAttr(nsAtom* aLocalName, nsAttrValue& aValue,
                                   bool* aHadValue) {
  *aHadValue = false;

  for (InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(aLocalName)) {
      attr.mValue.SwapValueWith(aValue);
      *aHadValue = true;
      return NS_OK;
    }
  }

  return AddNewAttribute(aLocalName, aValue);
}

nsresult AttrArray::SetAndSwapAttr(mozilla::dom::NodeInfo* aName,
                                   nsAttrValue& aValue, bool* aHadValue) {
  int32_t namespaceID = aName->NamespaceID();
  nsAtom* localName = aName->NameAtom();
  if (namespaceID == kNameSpaceID_None) {
    return SetAndSwapAttr(localName, aValue, aHadValue);
  }

  *aHadValue = false;
  for (InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(localName, namespaceID)) {
      attr.mName.SetTo(aName);
      attr.mValue.SwapValueWith(aValue);
      *aHadValue = true;
      return NS_OK;
    }
  }

  return AddNewAttribute(aName, aValue);
}

nsresult AttrArray::RemoveAttrAt(uint32_t aPos, nsAttrValue& aValue) {
  NS_ASSERTION(aPos < AttrCount(), "out-of-bounds");

  mImpl->mBuffer[aPos].mValue.SwapValueWith(aValue);
  mImpl->mBuffer[aPos].~InternalAttr();

  memmove(mImpl->mBuffer + aPos, mImpl->mBuffer + aPos + 1,
          (mImpl->mAttrCount - aPos - 1) * sizeof(InternalAttr));

  --mImpl->mAttrCount;
  return NS_OK;
}

mozilla::dom::BorrowedAttrInfo AttrArray::AttrInfoAt(uint32_t aPos) const {
  NS_ASSERTION(aPos < AttrCount(), "out-of-bounds access in AttrArray");
  InternalAttr& attr = mImpl->mBuffer[aPos];
  return BorrowedAttrInfo(&attr.mName, &attr.mValue);
}

const nsAttrName* AttrArray::AttrNameAt(uint32_t aPos) const {
  NS_ASSERTION(aPos < AttrCount(), "out-of-bounds access in AttrArray");
  return &mImpl->mBuffer[aPos].mName;
}

const nsAttrName* AttrArray::GetSafeAttrNameAt(uint32_t aPos) const {
  if (aPos >= AttrCount()) {
    return nullptr;
  }
  return &mImpl->mBuffer[aPos].mName;
}

const nsAttrName* AttrArray::GetExistingAttrNameFromQName(
    const nsAString& aName) const {
  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.QualifiedNameEquals(aName)) {
      return &attr.mName;
    }
  }
  return nullptr;
}

int32_t AttrArray::IndexOfAttr(const nsAtom* aLocalName) const {
  int32_t i = 0;
  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(aLocalName)) {
      return i;
    }
    ++i;
  }
  return -1;
}

int32_t AttrArray::IndexOfAttr(const nsAtom* aLocalName,
                               int32_t aNamespaceID) const {
  if (aNamespaceID == kNameSpaceID_None) {
    // This should be the common case so lets use the optimized loop
    return IndexOfAttr(aLocalName);
  }
  int32_t i = 0;
  for (const InternalAttr& attr : Attrs()) {
    if (attr.mName.Equals(aLocalName, aNamespaceID)) {
      return i;
    }
    ++i;
  }
  return -1;
}

void AttrArray::Compact() {
  if (!mImpl) {
    return;
  }

  if (!mImpl->mAttrCount && !mImpl->mMappedAttributeBits) {
    mImpl.reset();
    return;
  }

  // Nothing to do.
  if (mImpl->mAttrCount == mImpl->mCapacity) {
    return;
  }

  Impl* impl = mImpl.release();
  impl = static_cast<Impl*>(
      realloc(impl, Impl::AllocationSizeForAttributes(impl->mAttrCount)));
  MOZ_ASSERT(impl, "failed to reallocate to a smaller buffer!");
  impl->mCapacity = impl->mAttrCount;
  mImpl.reset(impl);
}

nsresult AttrArray::EnsureCapacityToClone(const AttrArray& aOther) {
  MOZ_ASSERT(!mImpl,
             "AttrArray::EnsureCapacityToClone requires the array be empty "
             "when called");

  uint32_t attrCount = aOther.AttrCount();
  if (!attrCount) {
    return NS_OK;
  }

  // No need to use a CheckedUint32 because we are cloning. We know that we
  // have already allocated an AttrArray of this size.
  mImpl.reset(
      static_cast<Impl*>(malloc(Impl::AllocationSizeForAttributes(attrCount))));
  NS_ENSURE_TRUE(mImpl, NS_ERROR_OUT_OF_MEMORY);

  mImpl->mMappedAttributeBits = 0;
  mImpl->mCapacity = attrCount;
  mImpl->mAttrCount = 0;

  return NS_OK;
}

bool AttrArray::GrowBy(uint32_t aGrowSize) {
  const uint32_t kLinearThreshold = 16;
  const uint32_t kLinearGrowSize = 4;

  CheckedUint32 capacity = mImpl ? mImpl->mCapacity : 0;
  CheckedUint32 minCapacity = capacity;
  minCapacity += aGrowSize;
  if (!minCapacity.isValid()) {
    return false;
  }

  if (capacity.value() <= kLinearThreshold) {
    do {
      capacity += kLinearGrowSize;
      if (!capacity.isValid()) {
        return false;
      }
    } while (capacity.value() < minCapacity.value());
  } else {
    uint32_t shift = mozilla::CeilingLog2(minCapacity.value());
    if (shift >= 32) {
      return false;
    }
    capacity = 1u << shift;
  }

  CheckedUint32 sizeInBytes = capacity.value();
  sizeInBytes *= sizeof(InternalAttr);
  if (!sizeInBytes.isValid()) {
    return false;
  }

  sizeInBytes += sizeof(Impl);
  if (!sizeInBytes.isValid()) {
    return false;
  }

  MOZ_ASSERT(sizeInBytes.value() ==
             Impl::AllocationSizeForAttributes(capacity.value()));

  const bool needToInitialize = !mImpl;
  Impl* newImpl =
      static_cast<Impl*>(realloc(mImpl.release(), sizeInBytes.value()));
  NS_ENSURE_TRUE(newImpl, false);

  mImpl.reset(newImpl);

  // Set initial counts if we didn't have a buffer before
  if (needToInitialize) {
    mImpl->mMappedAttributeBits = 0;
    mImpl->mAttrCount = 0;
  }

  mImpl->mCapacity = capacity.value();
  return true;
}

size_t AttrArray::SizeOfExcludingThis(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  if (!mImpl) {
    return 0;
  }
  size_t n = aMallocSizeOf(mImpl.get());
  for (const InternalAttr& attr : Attrs()) {
    n += attr.mValue.SizeOfExcludingThis(aMallocSizeOf);
  }
  return n;
}

int32_t AttrArray::FindAttrValueIn(int32_t aNameSpaceID, const nsAtom* aName,
                                   AttrValuesArray* aValues,
                                   nsCaseTreatment aCaseSensitive) const {
  NS_ASSERTION(aName, "Must have attr name");
  NS_ASSERTION(aNameSpaceID != kNameSpaceID_Unknown, "Must have namespace");
  NS_ASSERTION(aValues, "Null value array");

  const nsAttrValue* val = GetAttr(aName, aNameSpaceID);
  if (val) {
    for (int32_t i = 0; aValues[i]; ++i) {
      if (val->Equals(aValues[i], aCaseSensitive)) {
        return i;
      }
    }
    return ATTR_VALUE_NO_MATCH;
  }
  return ATTR_MISSING;
}
