/* 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 "MediaElementEventRunners.h"

#include <stdint.h>

#include "MediaProfilerMarkers.h"
#include "mozilla/Casting.h"
#include "mozilla/FlowMarkers.h"
#include "mozilla/ProfilerState.h"
#include "mozilla/dom/HTMLMediaElement.h"
#include "mozilla/dom/HTMLVideoElement.h"
#include "mozilla/dom/MediaError.h"
#include "mozilla/dom/TimeRanges.h"

extern mozilla::LazyLogModule gMediaElementEventsLog;
#define LOG_EVENT(type, msg) \
  MOZ_LOG_FMT(gMediaElementEventsLog, type, MOZ_LOG_EXPAND_ARGS msg)

namespace mozilla::dom {

nsMediaEventRunner::nsMediaEventRunner(const char* aName,
                                       HTMLMediaElement* aElement,
                                       const nsAString& aEventName)
    : mElement(aElement),
      mName(aName),
      mEventName(aEventName),
      mLoadID(mElement->GetCurrentLoadID()) {}

bool nsMediaEventRunner::IsCancelled() const {
  return !mElement || mElement->GetCurrentLoadID() != mLoadID;
}

nsresult nsMediaEventRunner::FireEvent(const nsAString& aName) {
  nsresult rv = NS_OK;
  if (mElement) {
    ReportProfilerMarker();
    rv = RefPtr{mElement}->FireEvent(aName);
  }
  return rv;
}

void nsMediaEventRunner::ReportProfilerMarker() {
  if (!profiler_is_collecting_markers()) {
    return;
  }
  // Report the buffered range.
  if (mEventName.EqualsLiteral("progress")) {
    RefPtr<TimeRanges> buffered = mElement->Buffered();
    if (buffered && buffered->Length() > 0) {
      for (size_t i = 0; i < buffered->Length(); ++i) {
        profiler_add_marker("progress", geckoprofiler::category::MEDIA_PLAYBACK,
                            {}, BufferedUpdateMarker{},
                            AssertedCast<uint64_t>(buffered->Start(i) * 1000),
                            AssertedCast<uint64_t>(buffered->End(i) * 1000),
                            GetElementDurationMs(),
                            Flow::FromPointer(mElement.get()));
      }
    }
  } else if (mEventName.EqualsLiteral("resize")) {
    auto mediaInfo = mElement->GetMediaInfo();
    profiler_add_marker("resize", geckoprofiler::category::MEDIA_PLAYBACK, {},
                        VideoResizeMarker{}, mediaInfo.mVideo.mDisplay.width,
                        mediaInfo.mVideo.mDisplay.height,
                        Flow::FromPointer(mElement.get()));
  } else if (mEventName.EqualsLiteral("loadedmetadata")) {
    nsString src;
    mElement->GetCurrentSrc(src);
    auto mediaInfo = mElement->GetMediaInfo();
    profiler_add_marker(
        "loadedmetadata", geckoprofiler::category::MEDIA_PLAYBACK, {},
        MetadataMarker{}, src,
        mediaInfo.HasAudio() ? mediaInfo.mAudio.mMimeType : "none"_ns,
        mediaInfo.HasVideo() ? mediaInfo.mVideo.mMimeType : "none"_ns,
        Flow::FromPointer(mElement.get()));
  } else if (mEventName.EqualsLiteral("error")) {
    auto* error = mElement->GetError();
    nsString message;
    error->GetMessage(message);
    profiler_add_marker("error", geckoprofiler::category::MEDIA_PLAYBACK, {},
                        ErrorMarker{}, message,
                        Flow::FromPointer(mElement.get()));
  } else {
    auto eventName = NS_ConvertUTF16toUTF8(mEventName);
    PROFILER_MARKER(eventName, MEDIA_PLAYBACK, {}, FlowMarker,
                    Flow::FromPointer(mElement.get()));
  }
}

uint64_t nsMediaEventRunner::GetElementDurationMs() const {
  MOZ_ASSERT(!IsCancelled());
  double duration = mElement->Duration();

  if (duration == std::numeric_limits<double>::infinity()) {
    return std::numeric_limits<uint64_t>::max();
  }

  if (std::isnan(duration) || duration <= 0) {
    // Duration is unknown or invalid
    return 0;
  }
  return AssertedCast<uint64_t>(duration * 1000);
}

NS_IMPL_CYCLE_COLLECTION(nsMediaEventRunner, mElement)
NS_IMPL_CYCLE_COLLECTING_ADDREF(nsMediaEventRunner)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsMediaEventRunner)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsMediaEventRunner)
  NS_INTERFACE_MAP_ENTRY(nsINamed)
  NS_INTERFACE_MAP_ENTRY(nsIRunnable)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRunnable)
NS_INTERFACE_MAP_END

NS_IMETHODIMP nsAsyncEventRunner::Run() {
  // Silently cancel if our load has been cancelled or element has been CCed.
  return IsCancelled() ? NS_OK : FireEvent(mEventName);
}

nsResolveOrRejectPendingPlayPromisesRunner::
    nsResolveOrRejectPendingPlayPromisesRunner(
        HTMLMediaElement* aElement, nsTArray<RefPtr<PlayPromise>>&& aPromises,
        nsresult aError)
    : nsMediaEventRunner("nsResolveOrRejectPendingPlayPromisesRunner",
                         aElement),
      mPromises(std::move(aPromises)),
      mError(aError) {
  mElement->mPendingPlayPromisesRunners.AppendElement(this);
}

void nsResolveOrRejectPendingPlayPromisesRunner::ResolveOrReject() {
  if (NS_SUCCEEDED(mError)) {
    PlayPromise::ResolvePromisesWithUndefined(mPromises);
  } else {
    PlayPromise::RejectPromises(mPromises, mError);
  }
}

NS_IMETHODIMP nsResolveOrRejectPendingPlayPromisesRunner::Run() {
  if (!IsCancelled()) {
    ResolveOrReject();
  }

  mElement->mPendingPlayPromisesRunners.RemoveElement(this);
  return NS_OK;
}

NS_IMETHODIMP nsNotifyAboutPlayingRunner::Run() {
  if (!IsCancelled()) {
    FireEvent(u"playing"_ns);
  }
  return nsResolveOrRejectPendingPlayPromisesRunner::Run();
}

NS_IMPL_CYCLE_COLLECTION_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner,
                                   nsMediaEventRunner, mPromises)
NS_IMPL_ADDREF_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner,
                         nsMediaEventRunner)
NS_IMPL_RELEASE_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner,
                          nsMediaEventRunner)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
    nsResolveOrRejectPendingPlayPromisesRunner)
NS_INTERFACE_MAP_END_INHERITING(nsMediaEventRunner)

NS_IMETHODIMP nsSourceErrorEventRunner::Run() {
  // Silently cancel if our load has been cancelled.
  if (IsCancelled()) {
    return NS_OK;
  }
  LOG_EVENT(LogLevel::Debug, ("{} Dispatching simple event source error",
                              fmt::ptr(mElement.get())));
  if (profiler_is_collecting_markers()) {
    profiler_add_marker("sourceerror", geckoprofiler::category::MEDIA_PLAYBACK,
                        {}, ErrorMarker{}, mErrorDetails,
                        Flow::FromPointer(mElement.get()));
  }
  return nsContentUtils::DispatchTrustedEvent(mElement->OwnerDoc(), mSource,
                                              u"error"_ns, CanBubble::eNo,
                                              Cancelable::eNo);
}

NS_IMPL_CYCLE_COLLECTION_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner,
                                   mSource)
NS_IMPL_ADDREF_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner)
NS_IMPL_RELEASE_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsSourceErrorEventRunner)
NS_INTERFACE_MAP_END_INHERITING(nsMediaEventRunner)

NS_IMETHODIMP nsTimeupdateRunner::Run() {
  if (IsCancelled() || !ShouldDispatchTimeupdate()) {
    return NS_OK;
  }
  // After dispatching `timeupdate`, if the timeupdate event listener takes lots
  // of time then we end up spending all time handling just timeupdate events.
  // The spec is vague in this situation, so we choose to update time after we
  // dispatch the event in order to solve that issue.
  nsresult rv = FireEvent(mEventName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG_EVENT(LogLevel::Debug,
              ("{} Failed to dispatch 'timeupdate'", fmt::ptr(mElement.get())));
  } else {
    mElement->UpdateLastTimeupdateDispatchTime();
  }
  return rv;
}

bool nsTimeupdateRunner::ShouldDispatchTimeupdate() const {
  if (mIsMandatory) {
    return true;
  }

  // If the main thread is busy, tasks may be delayed and dispatched at
  // unexpected times. Ensure we don't dispatch `timeupdate` more often
  // than once per `TIMEUPDATE_MS`.
  const TimeStamp& lastTime = mElement->LastTimeupdateDispatchTime();
  return lastTime.IsNull() || TimeStamp::Now() - lastTime >
                                  TimeDuration::FromMilliseconds(TIMEUPDATE_MS);
}

void nsTimeupdateRunner::ReportProfilerMarker() {
  if (!profiler_is_collecting_markers()) {
    return;
  }
  auto* videoElement = mElement->AsHTMLVideoElement();
  profiler_add_marker("timeupdate", geckoprofiler::category::MEDIA_PLAYBACK, {},
                      TimeUpdateMarker{},
                      AssertedCast<uint64_t>(mElement->CurrentTime() * 1000),
                      GetElementDurationMs(),
                      videoElement ? videoElement->MozPaintedFrames() : 0,
                      Flow::FromPointer(mElement.get()));
}

#undef LOG_EVENT
}  // namespace mozilla::dom
