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

#include "MFMediaEngineUtils.h"
#include "RemoteDecoderManagerChild.h"
#include "mozilla/WindowsVersion.h"

#ifdef MOZ_WMF_CDM
#  include "WMFCDMProxy.h"
#endif

namespace mozilla {

#define CLOG(msg, ...)                                                      \
  MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug,                               \
          ("MFMediaEngineChild=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
           ##__VA_ARGS__))

#define WLOG(msg, ...)                                                        \
  MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug,                                 \
          ("MFMediaEngineWrapper=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
           ##__VA_ARGS__))

#define WLOGV(msg, ...)                                                       \
  MOZ_LOG(gMFMediaEngineLog, LogLevel::Verbose,                               \
          ("MFMediaEngineWrapper=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
           ##__VA_ARGS__))

using media::TimeUnit;

MFMediaEngineChild::MFMediaEngineChild(MFMediaEngineWrapper* aOwner,
                                       FrameStatistics* aFrameStats)
    : mOwner(aOwner),
      mManagerThread(RemoteDecoderManagerChild::GetManagerThread()),
      mMediaEngineId(0 /* invalid id, will be initialized later */),
      mFrameStats(WrapNotNull(aFrameStats)) {
  if (mFrameStats->GetPresentedFrames() > 0) {
    mAccumulatedPresentedFramesFromPrevEngine =
        Some(mFrameStats->GetPresentedFrames());
  }
  if (mFrameStats->GetDroppedSinkFrames() > 0) {
    mAccumulatedDroppedFramesFromPrevEngine =
        Some(mFrameStats->GetDroppedSinkFrames());
  }
}

RefPtr<GenericNonExclusivePromise> MFMediaEngineChild::Init(
    bool aShouldPreload) {
  if (!mManagerThread) {
    return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
                                                       __func__);
  }

  if (!IsWin10OrLater()) {
    CLOG("Only support MF media engine playback on Windows 10+");
    return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
                                                       __func__);
  }

  CLOG("Init");
  MOZ_ASSERT(mMediaEngineId == 0);
  RefPtr<MFMediaEngineChild> self = this;
  RemoteDecoderManagerChild::LaunchUtilityProcessIfNeeded(
      RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM)
      ->Then(
          mManagerThread, __func__,
          [self, this, aShouldPreload](bool) {
            RefPtr<RemoteDecoderManagerChild> manager =
                RemoteDecoderManagerChild::GetSingleton(
                    RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
            if (!manager || !manager->CanSend()) {
              CLOG("Manager not exists or can't send");
              mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
              return;
            }

            mIPDLSelfRef = this;
            Unused << manager->SendPMFMediaEngineConstructor(this);
            MediaEngineInfoIPDL info(aShouldPreload);
            SendInitMediaEngine(info)
                ->Then(
                    mManagerThread, __func__,
                    [self, this](uint64_t aId) {
                      mInitEngineRequest.Complete();
                      // Id 0 is used to indicate error.
                      if (aId == 0) {
                        CLOG("Failed to initialize MFMediaEngineChild");
                        mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE,
                                                          __func__);
                        return;
                      }
                      mMediaEngineId = aId;
                      CLOG("Initialized MFMediaEngineChild");
                      mInitPromiseHolder.ResolveIfExists(true, __func__);
                    },
                    [self,
                     this](const mozilla::ipc::ResponseRejectReason& aReason) {
                      mInitEngineRequest.Complete();
                      CLOG(
                          "Failed to initialize MFMediaEngineChild due to "
                          "IPC failure");
                      mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE,
                                                        __func__);
                    })
                ->Track(mInitEngineRequest);
          },
          [self, this](nsresult aResult) {
            CLOG("SendInitMediaEngine Failed");
            self->mInitPromiseHolder.Reject(NS_ERROR_FAILURE, __func__);
          });
  return mInitPromiseHolder.Ensure(__func__);
}

mozilla::ipc::IPCResult MFMediaEngineChild::RecvRequestSample(TrackType aType,
                                                              bool aIsEnough) {
  AssertOnManagerThread();
  if (!mOwner) {
    return IPC_OK();
  }
  if (aType == TrackType::kVideoTrack) {
    mOwner->NotifyEvent(aIsEnough ? ExternalEngineEvent::VideoEnough
                                  : ExternalEngineEvent::RequestForVideo);
  } else if (aType == TrackType::kAudioTrack) {
    mOwner->NotifyEvent(aIsEnough ? ExternalEngineEvent::AudioEnough
                                  : ExternalEngineEvent::RequestForAudio);
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult MFMediaEngineChild::RecvUpdateCurrentTime(
    double aCurrentTimeInSecond) {
  AssertOnManagerThread();
  if (mOwner) {
    mOwner->UpdateCurrentTime(aCurrentTimeInSecond);
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult MFMediaEngineChild::RecvNotifyEvent(
    MFMediaEngineEvent aEvent) {
  AssertOnManagerThread();
  switch (aEvent) {
    case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY:
      mOwner->NotifyEvent(ExternalEngineEvent::LoadedFirstFrame);
      break;
    case MF_MEDIA_ENGINE_EVENT_LOADEDDATA:
      mOwner->NotifyEvent(ExternalEngineEvent::LoadedData);
      break;
    case MF_MEDIA_ENGINE_EVENT_WAITING:
      mOwner->NotifyEvent(ExternalEngineEvent::Waiting);
      break;
    case MF_MEDIA_ENGINE_EVENT_SEEKED:
      mOwner->NotifyEvent(ExternalEngineEvent::Seeked);
      break;
    case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED:
      mOwner->NotifyEvent(ExternalEngineEvent::BufferingStarted);
      break;
    case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED:
      mOwner->NotifyEvent(ExternalEngineEvent::BufferingEnded);
      break;
    case MF_MEDIA_ENGINE_EVENT_ENDED:
      mOwner->NotifyEvent(ExternalEngineEvent::Ended);
      break;
    case MF_MEDIA_ENGINE_EVENT_PLAYING:
      mOwner->NotifyEvent(ExternalEngineEvent::Playing);
      break;
    default:
      NS_WARNING(
          nsPrintfCString("Unhandled event=%s", MediaEngineEventToStr(aEvent))
              .get());
      break;
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult MFMediaEngineChild::RecvNotifyError(
    const MediaResult& aError) {
  AssertOnManagerThread();
  mOwner->NotifyError(aError);
  return IPC_OK();
}

mozilla::ipc::IPCResult MFMediaEngineChild::RecvUpdateStatisticData(
    const StatisticData& aData) {
  AssertOnManagerThread();
  const uint64_t currentRenderedFrames = mFrameStats->GetPresentedFrames();
  const uint64_t newRenderedFrames = GetUpdatedRenderedFrames(aData);
  // Media engine won't tell us that which stage those dropped frames happened,
  // so we treat all of them as the frames dropped in the a/v sync stage (sink).
  const uint64_t currentDroppedSinkFrames = mFrameStats->GetDroppedSinkFrames();
  const uint64_t newDroppedSinkFrames = GetUpdatedDroppedFrames(aData);
  mFrameStats->Accumulate({0, 0, newRenderedFrames - currentRenderedFrames, 0,
                           newDroppedSinkFrames - currentDroppedSinkFrames, 0});
  CLOG("Update statictis data (rendered %" PRIu64 " -> %" PRIu64
       ", dropped %" PRIu64 " -> %" PRIu64 ")",
       currentRenderedFrames, mFrameStats->GetPresentedFrames(),
       currentDroppedSinkFrames, mFrameStats->GetDroppedSinkFrames());
  MOZ_ASSERT(mFrameStats->GetPresentedFrames() >= currentRenderedFrames);
  MOZ_ASSERT(mFrameStats->GetDroppedSinkFrames() >= currentDroppedSinkFrames);
  return IPC_OK();
}

uint64_t MFMediaEngineChild::GetUpdatedRenderedFrames(
    const StatisticData& aData) {
  return mAccumulatedPresentedFramesFromPrevEngine
             ? (aData.renderedFrames() +
                *mAccumulatedPresentedFramesFromPrevEngine)
             : aData.renderedFrames();
}

uint64_t MFMediaEngineChild::GetUpdatedDroppedFrames(
    const StatisticData& aData) {
  return mAccumulatedDroppedFramesFromPrevEngine
             ? (aData.droppedFrames() +
                *mAccumulatedDroppedFramesFromPrevEngine)
             : aData.droppedFrames();
}

void MFMediaEngineChild::OwnerDestroyed() {
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineChild::OwnerDestroy", [self = RefPtr{this}, this] {
        self->mOwner = nullptr;
        // Ask to destroy IPDL.
        if (CanSend()) {
          MFMediaEngineChild::Send__delete__(this);
        }
      }));
}

void MFMediaEngineChild::IPDLActorDestroyed() {
  AssertOnManagerThread();
  if (!mShutdown) {
    CLOG("Destroyed actor without shutdown, remote process has crashed!");
    mOwner->NotifyError(NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR);
  }
  mIPDLSelfRef = nullptr;
}

void MFMediaEngineChild::Shutdown() {
  AssertOnManagerThread();
  if (mShutdown) {
    return;
  }
  SendShutdown();
  mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
  mInitEngineRequest.DisconnectIfExists();
  mShutdown = true;
}

MFMediaEngineWrapper::MFMediaEngineWrapper(ExternalEngineStateMachine* aOwner,
                                           FrameStatistics* aFrameStats)
    : ExternalPlaybackEngine(aOwner),
      mEngine(new MFMediaEngineChild(this, aFrameStats)),
      mCurrentTimeInSecond(0.0) {}

RefPtr<GenericNonExclusivePromise> MFMediaEngineWrapper::Init(
    bool aShouldPreload) {
  WLOG("Init");
  return mEngine->Init(aShouldPreload);
}

MFMediaEngineWrapper::~MFMediaEngineWrapper() { mEngine->OwnerDestroyed(); }

void MFMediaEngineWrapper::Play() {
  WLOG("Play");
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(
      NS_NewRunnableFunction("MFMediaEngineWrapper::Play",
                             [engine = mEngine] { engine->SendPlay(); }));
}

void MFMediaEngineWrapper::Pause() {
  WLOG("Pause");
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(
      NS_NewRunnableFunction("MFMediaEngineWrapper::Pause",
                             [engine = mEngine] { engine->SendPause(); }));
}

void MFMediaEngineWrapper::Seek(const TimeUnit& aTargetTime) {
  auto currentTimeInSecond = aTargetTime.ToSeconds();
  mCurrentTimeInSecond = currentTimeInSecond;
  WLOG("Seek to %f", currentTimeInSecond);
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineWrapper::Seek", [engine = mEngine, currentTimeInSecond] {
        engine->SendSeek(currentTimeInSecond);
      }));
}

void MFMediaEngineWrapper::Shutdown() {
  WLOG("Shutdown");
  Unused << ManagerThread()->Dispatch(
      NS_NewRunnableFunction("MFMediaEngineWrapper::Shutdown",
                             [engine = mEngine] { engine->Shutdown(); }));
}

void MFMediaEngineWrapper::SetPlaybackRate(double aPlaybackRate) {
  WLOG("Set playback rate %f", aPlaybackRate);
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(
      NS_NewRunnableFunction("MFMediaEngineWrapper::SetPlaybackRate",
                             [engine = mEngine, aPlaybackRate] {
                               engine->SendSetPlaybackRate(aPlaybackRate);
                             }));
}

void MFMediaEngineWrapper::SetVolume(double aVolume) {
  WLOG("Set volume %f", aVolume);
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineWrapper::SetVolume",
      [engine = mEngine, aVolume] { engine->SendSetVolume(aVolume); }));
}

void MFMediaEngineWrapper::SetLooping(bool aLooping) {
  WLOG("Set looping %d", aLooping);
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineWrapper::SetLooping",
      [engine = mEngine, aLooping] { engine->SendSetLooping(aLooping); }));
}

void MFMediaEngineWrapper::SetPreservesPitch(bool aPreservesPitch) {
  // Media Engine doesn't support this.
}

void MFMediaEngineWrapper::NotifyEndOfStream(TrackInfo::TrackType aType) {
  WLOG("NotifyEndOfStream, type=%s", TrackTypeToStr(aType));
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineWrapper::NotifyEndOfStream",
      [engine = mEngine, aType] { engine->SendNotifyEndOfStream(aType); }));
}

void MFMediaEngineWrapper::SetMediaInfo(const MediaInfo& aInfo) {
  WLOG("SetMediaInfo, hasAudio=%d, hasVideo=%d, encrypted=%d", aInfo.HasAudio(),
       aInfo.HasVideo(), aInfo.IsEncrypted());
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineWrapper::SetMediaInfo", [engine = mEngine, aInfo] {
        MediaInfoIPDL info(aInfo.HasAudio() ? Some(aInfo.mAudio) : Nothing(),
                           aInfo.HasVideo() ? Some(aInfo.mVideo) : Nothing());
        engine->SendNotifyMediaInfo(info);
      }));
}

bool MFMediaEngineWrapper::SetCDMProxy(CDMProxy* aProxy) {
#ifdef MOZ_WMF_CDM
  WMFCDMProxy* proxy = aProxy->AsWMFCDMProxy();
  if (!proxy) {
    WLOG("Only WFMCDM Proxy is supported for the media engine!");
    return false;
  }

  const uint64_t proxyId = proxy->GetCDMProxyId();
  WLOG("SetCDMProxy, CDM-Id=%" PRIu64, proxyId);
  MOZ_ASSERT(IsInited());
  Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
      "MFMediaEngineWrapper::SetCDMProxy",
      [engine = mEngine, proxyId] { engine->SendSetCDMProxyId(proxyId); }));
  return true;
#else
  return false;
#endif
}

TimeUnit MFMediaEngineWrapper::GetCurrentPosition() {
  return TimeUnit::FromSeconds(mCurrentTimeInSecond);
}

void MFMediaEngineWrapper::UpdateCurrentTime(double aCurrentTimeInSecond) {
  AssertOnManagerThread();
  WLOGV("Update current time %f", aCurrentTimeInSecond);
  mCurrentTimeInSecond = aCurrentTimeInSecond;
  NotifyEvent(ExternalEngineEvent::Timeupdate);
}

void MFMediaEngineWrapper::NotifyEvent(ExternalEngineEvent aEvent) {
  AssertOnManagerThread();
  WLOGV("Received event %s", ExternalEngineEventToStr(aEvent));
  mOwner->NotifyEvent(aEvent);
}

void MFMediaEngineWrapper::NotifyError(const MediaResult& aError) {
  AssertOnManagerThread();
  WLOG("Received error: %s", aError.Description().get());
  mOwner->NotifyError(aError);
}

}  // namespace mozilla
