/* 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 nsQueryFrame_h
#define nsQueryFrame_h

#include <type_traits>

#include "mozilla/Assertions.h"
#include "nscore.h"

namespace mozilla {
class ScrollContainerFrame;
}

#define NS_DECL_QUERYFRAME_TARGET(classname)      \
  static const nsQueryFrame::FrameIID kFrameIID = \
      nsQueryFrame::classname##_id;               \
  typedef classname Has_NS_DECL_QUERYFRAME_TARGET;

#define NS_DECL_QUERYFRAME void* QueryFrame(FrameIID id) const override;

#define NS_QUERYFRAME_HEAD(class)               \
  void* class ::QueryFrame(FrameIID id) const { \
    switch (id) {
#define NS_QUERYFRAME_ENTRY(class)                                    \
  case class ::kFrameIID: {                                           \
    static_assert(                                                    \
        std::is_same_v<class, class ::Has_NS_DECL_QUERYFRAME_TARGET>, \
        #class " must declare itself as a queryframe target");        \
    return const_cast<class*>(static_cast<const class*>(this));       \
  }

#define NS_QUERYFRAME_ENTRY_CONDITIONAL(class, condition)               \
  case class ::kFrameIID:                                               \
    if (condition) {                                                    \
      static_assert(                                                    \
          std::is_same_v<class, class ::Has_NS_DECL_QUERYFRAME_TARGET>, \
          #class " must declare itself as a queryframe target");        \
      return const_cast<class*>(static_cast<const class*>(this));       \
    }                                                                   \
    break;

#define NS_QUERYFRAME_TAIL_INHERITING(class) \
  default:                                   \
    break;                                   \
    }                                        \
    return class ::QueryFrame(id);           \
    }

#define NS_QUERYFRAME_TAIL_INHERITANCE_ROOT                          \
  default:                                                           \
    break;                                                           \
    }                                                                \
    MOZ_ASSERT(id != GetFrameId(),                                   \
               "A frame failed to QueryFrame to its *own type*. "    \
               "It may be missing NS_DECL_QUERYFRAME, or a "         \
               "NS_QUERYFRAME_ENTRY() line with its own type name"); \
    return nullptr;                                                  \
    }

class nsQueryFrame {
 public:
  enum FrameIID {
#define FRAME_ID(classname, ...) classname##_id,
#define ABSTRACT_FRAME_ID(classname) classname##_id,
#include "mozilla/FrameIdList.h"
#undef FRAME_ID
#undef ABSTRACT_FRAME_ID
  };

  // A strict subset of FrameIID above for frame classes that we instantiate.
  enum class ClassID : uint8_t {
#define FRAME_ID(classname, ...) classname##_id,
#define ABSTRACT_FRAME_ID(classname)
#include "mozilla/FrameIdList.h"
#undef FRAME_ID
#undef ABSTRACT_FRAME_ID
  };

  virtual void* QueryFrame(FrameIID id) const = 0;
};

class nsIFrame;

namespace detail {
template <typename Dest, typename = void>
struct FastQueryFrame {
  static constexpr bool kSupported = false;
};

// For final classes we can check the class id.
template <typename Dest>
struct FastQueryFrame<Dest, std::enable_if_t<std::is_final_v<Dest>, void>> {
  static constexpr bool kSupported = true;

  template <typename Src>
  static Dest* QueryFrame(Src* aPtr) {
    return nsQueryFrame::FrameIID(aPtr->GetClassID()) == Dest::kFrameIID
               ? static_cast<Dest*>(aPtr)
               : nullptr;
  }
};

#define IMPL_FAST_QUERYFRAME(Dest_, Check_)                        \
  template <>                                                      \
  struct FastQueryFrame<Dest_, void> {                             \
    static constexpr bool kSupported = true;                       \
    template <typename Src>                                        \
    static Dest_* QueryFrame(Src* aPtr) {                          \
      return aPtr->Check_() ? static_cast<Dest_*>(aPtr) : nullptr; \
    }                                                              \
  }

IMPL_FAST_QUERYFRAME(mozilla::ScrollContainerFrame,
                     IsScrollContainerOrSubclass);

#undef IMPL_FAST_QUERYFRAME
}

template <typename Source>
class do_QueryFrameHelper {
 public:
  explicit do_QueryFrameHelper(Source* s) : mRawPtr(s) {}

  // The return and argument types here are arbitrarily selected so no
  // corresponding member function exists.
  using MatchNullptr = void (*)(double, float);
  // Implicit constructor for nullptr, trick borrowed from already_AddRefed.
  MOZ_IMPLICIT do_QueryFrameHelper(MatchNullptr aRawPtr) : mRawPtr(nullptr) {}

  template <typename Dest>
  operator Dest*() {
    static_assert(std::is_same_v<std::remove_const_t<Dest>,
                                 typename Dest::Has_NS_DECL_QUERYFRAME_TARGET>,
                  "Dest must declare itself as a queryframe target");
    if (!mRawPtr) {
      return nullptr;
    }
    if constexpr (::detail::FastQueryFrame<Dest>::kSupported) {
      static_assert(
          std::is_base_of_v<nsIFrame, Source>,
          "We only support fast do_QueryFrame() where the source must be a "
          "derived class of nsIFrame. Consider a two-step do_QueryFrame() "
          "(once to nsIFrame, another to the target) if absolutely needed.");
      Dest* f = ::detail::FastQueryFrame<Dest>::QueryFrame(mRawPtr);
      MOZ_ASSERT(
          f == reinterpret_cast<Dest*>(mRawPtr->QueryFrame(Dest::kFrameIID)),
          "fast and slow paths should give the same result");
      return f;
    }
    if constexpr (std::is_base_of_v<nsIFrame, Source> &&
                  std::is_base_of_v<nsIFrame, Dest>) {
      // For non-final frames we can still optimize the virtual call some of the
      // time.
      if (nsQueryFrame::FrameIID(mRawPtr->GetClassID()) == Dest::kFrameIID) {
        auto* f = static_cast<Dest*>(mRawPtr);
        MOZ_ASSERT(
            f == reinterpret_cast<Dest*>(mRawPtr->QueryFrame(Dest::kFrameIID)),
            "fast and slow paths should give the same result");
        return f;
      }
    }
    return reinterpret_cast<Dest*>(mRawPtr->QueryFrame(Dest::kFrameIID));
  }

 private:
  Source* mRawPtr;
};

template <typename T>
inline do_QueryFrameHelper<T> do_QueryFrame(T* s) {
  return do_QueryFrameHelper<T>(s);
}

#endif  // nsQueryFrame_h
