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

#ifndef mozilla_IntrinsicISizesCache_h
#define mozilla_IntrinsicISizesCache_h

#include "nsIFrame.h"

namespace mozilla {

// Some frame classes keep a cache of intrinsic inline sizes. This class
// encapsulates the logic for caching them depending on the IntrinsicSizeInput.
//
// The cache is intended to take as little space as possible
// (max(sizeof(nscoord) * 2, sizeof(void*))), when there are no percentage-size
// dependencies.
struct IntrinsicISizesCache final {
  IntrinsicISizesCache() {
    new (&mInline) InlineCache();
    MOZ_ASSERT(IsInline());
  }

  ~IntrinsicISizesCache() { delete GetOutOfLine(); }

  template <typename Compute>
  nscoord GetOrSet(nsIFrame& aFrame, IntrinsicISizeType aType,
                   const IntrinsicSizeInput& aInput, Compute aCompute) {
    bool dependentOnPercentBSize = aFrame.HasAnyStateBits(
        NS_FRAME_DESCENDANT_INTRINSIC_ISIZE_DEPENDS_ON_BSIZE);
    nscoord value = Get(dependentOnPercentBSize, aType, aInput);
    if (value != kNotFound) {
      return value;
    }
    value = aCompute();
    // Inside of aCompute(), we might have newly discovered that we do have a
    // descendant whose intrinsic isize depends on our bsize; so we check that
    // state bit again before updating the cache.
    dependentOnPercentBSize = aFrame.HasAnyStateBits(
        NS_FRAME_DESCENDANT_INTRINSIC_ISIZE_DEPENDS_ON_BSIZE);
    Set(dependentOnPercentBSize, aType, aInput, value);
    return value;
  }

  void Clear() {
    if (auto* ool = GetOutOfLine()) {
      ool->mCacheWithPercentageBasis.Clear();
      ool->mCacheWithoutPercentageBasis.Clear();
      ool->mLastPercentageBasis.reset();
    } else {
      mInline.Clear();
    }
  }

 private:
  // We use nscoord_MAX rather than NS_INTRINSIC_ISIZE_UNKNOWN as our sentinel
  // value so that our high bit is always free.
  static constexpr nscoord kNotFound = nscoord_MAX;

  nscoord Get(bool aDependentOnPercentBSize, IntrinsicISizeType aType,
              const IntrinsicSizeInput& aInput) const {
    const bool usePercentageAwareCache =
        aDependentOnPercentBSize && aInput.HasSomePercentageBasisForChildren();
    if (!usePercentageAwareCache) {
      if (auto* ool = GetOutOfLine()) {
        return ool->mCacheWithoutPercentageBasis.Get(aType);
      }
      return mInline.Get(aType);
    }
    if (auto* ool = GetOutOfLine()) {
      if (ool->mLastPercentageBasis == aInput.mPercentageBasisForChildren) {
        return ool->mCacheWithPercentageBasis.Get(aType);
      }
    }
    return kNotFound;
  }

  void Set(bool aDependentOnPercentBSize, IntrinsicISizeType aType,
           const IntrinsicSizeInput& aInput, nscoord aValue) {
    // Intrinsic sizes should be nonnegative, so this std::max clamping should
    // rarely be necessary except in cases of integer overflow.  We have to be
    // strict about it, though, because of how we (ab)use the high bit
    // (see kHighBit)
    aValue = std::max(aValue, 0);
    const bool usePercentageAwareCache =
        aDependentOnPercentBSize && aInput.HasSomePercentageBasisForChildren();
    if (usePercentageAwareCache) {
      auto* ool = EnsureOutOfLine();
      if (ool->mLastPercentageBasis != aInput.mPercentageBasisForChildren) {
        ool->mLastPercentageBasis = aInput.mPercentageBasisForChildren;
        ool->mCacheWithPercentageBasis.Clear();
      }
      ool->mCacheWithPercentageBasis.Set(aType, aValue);
    } else if (auto* ool = GetOutOfLine()) {
      ool->mCacheWithoutPercentageBasis.Set(aType, aValue);
    } else {
      mInline.Set(aType, aValue);
      // No inline value should be able to cause us to misinterpret our
      // representation as out-of-line, because intrinsic isizes should always
      // be non-negative.
      MOZ_DIAGNOSTIC_ASSERT(IsInline());
    }
  }

  struct InlineCache {
    nscoord mCachedMinISize = kNotFound;
    nscoord mCachedPrefISize = kNotFound;

    nscoord Get(IntrinsicISizeType aType) const {
      return aType == IntrinsicISizeType::MinISize ? mCachedMinISize
                                                   : mCachedPrefISize;
    }
    void Set(IntrinsicISizeType aType, nscoord aValue) {
      MOZ_ASSERT(aValue >= 0);
      if (aType == IntrinsicISizeType::MinISize) {
        mCachedMinISize = aValue;
      } else {
        mCachedPrefISize = aValue;
      }
    }

    void Clear() { *this = {}; }
  };

  struct OutOfLineCache {
    InlineCache mCacheWithoutPercentageBasis;
    InlineCache mCacheWithPercentageBasis;
    Maybe<LogicalSize> mLastPercentageBasis;
  };

  // If the high bit of mOutOfLine is 1, then it points to an OutOfLineCache.
  union {
    InlineCache mInline;
    struct {
#ifndef HAVE_64BIT_BUILD
      uintptr_t mPadding = 0;
#endif
      uintptr_t mOutOfLine = 0;
    };
  };

  static constexpr uintptr_t kHighBit = uintptr_t(1)
                                        << (sizeof(void*) * CHAR_BIT - 1);

  bool IsOutOfLine() const {
#ifdef HAVE_64BIT_BUILD
    return mOutOfLine & kHighBit;
#else
    return mPadding & kHighBit;
#endif
  }
  bool IsInline() const { return !IsOutOfLine(); }
  OutOfLineCache* EnsureOutOfLine() {
    if (auto* ool = GetOutOfLine()) {
      return ool;
    }
    auto inlineCache = mInline;
    auto* ool = new OutOfLineCache();
    ool->mCacheWithoutPercentageBasis = inlineCache;
#ifdef HAVE_64BIT_BUILD
    MOZ_ASSERT((reinterpret_cast<uintptr_t>(ool) & kHighBit) == 0);
    mOutOfLine = reinterpret_cast<uintptr_t>(ool) | kHighBit;
#else
    mOutOfLine = reinterpret_cast<uintptr_t>(ool);
    mPadding = kHighBit;
#endif
    MOZ_ASSERT(IsOutOfLine());
    return ool;
  }

  OutOfLineCache* GetOutOfLine() const {
    if (!IsOutOfLine()) {
      return nullptr;
    }
#ifdef HAVE_64BIT_BUILD
    return reinterpret_cast<OutOfLineCache*>(mOutOfLine & ~kHighBit);
#else
    return reinterpret_cast<OutOfLineCache*>(mOutOfLine);
#endif
  }
};

static_assert(sizeof(IntrinsicISizesCache) == 8, "Unexpected cache size");

}  // namespace mozilla

#endif
