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

#include "mozilla/MozPromise.h"
#include "MediaDecoderReaderWrapper.h"

namespace mozilla {

extern LazyLogModule gMediaDecoderLog;

#undef LOG
#define LOG(...) \
  MOZ_LOG(gMediaDecoderLog, mozilla::LogLevel::Debug, (__VA_ARGS__))

// StartTimeRendezvous is a helper class that quarantines the first sample
// until it gets a sample from both channels, such that we can be guaranteed
// to know the start time by the time On{Audio,Video}Decoded is called on MDSM.
class StartTimeRendezvous {
  typedef MediaDecoderReader::MediaDataPromise MediaDataPromise;
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(StartTimeRendezvous);

public:
  StartTimeRendezvous(AbstractThread* aOwnerThread,
                      bool aHasAudio,
                      bool aHasVideo,
                      bool aForceZeroStartTime)
    : mOwnerThread(aOwnerThread)
  {
    if (aForceZeroStartTime) {
      mAudioStartTime.emplace(0);
      mVideoStartTime.emplace(0);
      return;
    }
    if (!aHasAudio) {
      mAudioStartTime.emplace(INT64_MAX);
    }
    if (!aHasVideo) {
      mVideoStartTime.emplace(INT64_MAX);
    }
  }

  void Destroy()
  {
    mAudioStartTime = Some(mAudioStartTime.refOr(INT64_MAX));
    mVideoStartTime = Some(mVideoStartTime.refOr(INT64_MAX));
    mHaveStartTimePromise.RejectIfExists(false, __func__);
  }

  RefPtr<HaveStartTimePromise> AwaitStartTime()
  {
    if (HaveStartTime()) {
      return HaveStartTimePromise::CreateAndResolve(true, __func__);
    }
    return mHaveStartTimePromise.Ensure(__func__);
  }

  template<MediaData::Type SampleType>
  RefPtr<MediaDataPromise>
  ProcessFirstSample(MediaData* aData)
  {
    typedef typename MediaDataPromise::Private PromisePrivate;
    MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());

    MaybeSetChannelStartTime<SampleType>(aData->mTime);

    RefPtr<PromisePrivate> p = new PromisePrivate(__func__);
    RefPtr<MediaData> data = aData;
    RefPtr<StartTimeRendezvous> self = this;
    AwaitStartTime()->Then(
      mOwnerThread, __func__,
      [p, data, self] () {
        MOZ_ASSERT(self->mOwnerThread->IsCurrentThreadIn());
        p->Resolve(data, __func__);
      },
      [p] () {
        p->Reject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
      });

    return p.forget();
  }

  template<MediaData::Type SampleType>
  void FirstSampleRejected(const MediaResult& aError)
  {
    MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
    if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
      LOG("StartTimeRendezvous=%p SampleType(%d) Has no samples.",
           this, SampleType);
      MaybeSetChannelStartTime<SampleType>(INT64_MAX);
    } else if (aError != NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
      mHaveStartTimePromise.RejectIfExists(false, __func__);
    }
  }

  bool HaveStartTime() const
  {
    return mAudioStartTime.isSome() && mVideoStartTime.isSome();
  }

  int64_t StartTime() const
  {
    int64_t time = std::min(mAudioStartTime.ref(), mVideoStartTime.ref());
    return time == INT64_MAX ? 0 : time;
  }

private:
  ~StartTimeRendezvous() {}

  template<MediaData::Type SampleType>
  void MaybeSetChannelStartTime(int64_t aStartTime)
  {
    if (ChannelStartTime(SampleType).isSome()) {
      // If we're initialized with aForceZeroStartTime=true, the channel start
      // times are already set.
      return;
    }

    LOG("StartTimeRendezvous=%p Setting SampleType(%d) start time to %lld",
        this, SampleType, aStartTime);

    ChannelStartTime(SampleType).emplace(aStartTime);
    if (HaveStartTime()) {
      mHaveStartTimePromise.ResolveIfExists(true, __func__);
    }
  }

  Maybe<int64_t>& ChannelStartTime(MediaData::Type aType)
  {
    return aType == MediaData::AUDIO_DATA ? mAudioStartTime : mVideoStartTime;
  }

  MozPromiseHolder<HaveStartTimePromise> mHaveStartTimePromise;
  RefPtr<AbstractThread> mOwnerThread;
  Maybe<int64_t> mAudioStartTime;
  Maybe<int64_t> mVideoStartTime;
};

MediaDecoderReaderWrapper::MediaDecoderReaderWrapper(AbstractThread* aOwnerThread,
                                                     MediaDecoderReader* aReader)
  : mForceZeroStartTime(aReader->ForceZeroStartTime())
  , mOwnerThread(aOwnerThread)
  , mReader(aReader)
{}

MediaDecoderReaderWrapper::~MediaDecoderReaderWrapper()
{}

media::TimeUnit
MediaDecoderReaderWrapper::StartTime() const
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  MOZ_ASSERT(!mShutdown);
  return media::TimeUnit::FromMicroseconds(mStartTimeRendezvous->StartTime());
}

RefPtr<MediaDecoderReaderWrapper::MetadataPromise>
MediaDecoderReaderWrapper::ReadMetadata()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  MOZ_ASSERT(!mShutdown);
  return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                     &MediaDecoderReader::AsyncReadMetadata)
         ->Then(mOwnerThread, __func__, this,
                &MediaDecoderReaderWrapper::OnMetadataRead,
                &MediaDecoderReaderWrapper::OnMetadataNotRead)
         ->CompletionPromise();
}

RefPtr<HaveStartTimePromise>
MediaDecoderReaderWrapper::AwaitStartTime()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  MOZ_ASSERT(!mShutdown);
  return mStartTimeRendezvous->AwaitStartTime();
}

void
MediaDecoderReaderWrapper::RequestAudioData()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  MOZ_ASSERT(!mShutdown);

  auto p = InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                       &MediaDecoderReader::RequestAudioData);

  if (!mStartTimeRendezvous->HaveStartTime()) {
    p = p->Then(mOwnerThread, __func__, mStartTimeRendezvous.get(),
                &StartTimeRendezvous::ProcessFirstSample<MediaData::AUDIO_DATA>,
                &StartTimeRendezvous::FirstSampleRejected<MediaData::AUDIO_DATA>)
         ->CompletionPromise();
  }

  RefPtr<MediaDecoderReaderWrapper> self = this;
  mAudioDataRequest.Begin(p->Then(mOwnerThread, __func__,
    [self] (MediaData* aAudioSample) {
      self->mAudioDataRequest.Complete();
      aAudioSample->AdjustForStartTime(self->StartTime().ToMicroseconds());
      self->mAudioCallback.Notify(AsVariant(aAudioSample));
    },
    [self] (const MediaResult& aError) {
      self->mAudioDataRequest.Complete();
      self->mAudioCallback.Notify(AsVariant(aError));
    }));
}

void
MediaDecoderReaderWrapper::RequestVideoData(bool aSkipToNextKeyframe,
                                            media::TimeUnit aTimeThreshold)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  MOZ_ASSERT(!mShutdown);

  // Time the video decode and send this value back to callbacks who accept
  // a TimeStamp as its second parameter.
  TimeStamp videoDecodeStartTime = TimeStamp::Now();

  if (aTimeThreshold.ToMicroseconds() > 0 &&
      mStartTimeRendezvous->HaveStartTime()) {
    aTimeThreshold += StartTime();
  }

  auto p = InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                       &MediaDecoderReader::RequestVideoData,
                       aSkipToNextKeyframe, aTimeThreshold.ToMicroseconds());

  if (!mStartTimeRendezvous->HaveStartTime()) {
    p = p->Then(mOwnerThread, __func__, mStartTimeRendezvous.get(),
                &StartTimeRendezvous::ProcessFirstSample<MediaData::VIDEO_DATA>,
                &StartTimeRendezvous::FirstSampleRejected<MediaData::VIDEO_DATA>)
         ->CompletionPromise();
  }

  RefPtr<MediaDecoderReaderWrapper> self = this;
  mVideoDataRequest.Begin(p->Then(mOwnerThread, __func__,
    [self, videoDecodeStartTime] (MediaData* aVideoSample) {
      self->mVideoDataRequest.Complete();
      aVideoSample->AdjustForStartTime(self->StartTime().ToMicroseconds());
      self->mVideoCallback.Notify(AsVariant(MakeTuple(aVideoSample, videoDecodeStartTime)));
    },
    [self] (const MediaResult& aError) {
      self->mVideoDataRequest.Complete();
      self->mVideoCallback.Notify(AsVariant(aError));
    }));
}

bool
MediaDecoderReaderWrapper::IsRequestingAudioData() const
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return mAudioDataRequest.Exists();
}

bool
MediaDecoderReaderWrapper::IsRequestingVideoData() const
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return mVideoDataRequest.Exists();
}

bool
MediaDecoderReaderWrapper::IsWaitingAudioData() const
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return mAudioWaitRequest.Exists();
}

bool
MediaDecoderReaderWrapper::IsWaitingVideoData() const
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return mVideoWaitRequest.Exists();
}

RefPtr<MediaDecoderReader::SeekPromise>
MediaDecoderReaderWrapper::Seek(SeekTarget aTarget, media::TimeUnit aEndTime)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  aTarget.SetTime(aTarget.GetTime() + StartTime());
  return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                     &MediaDecoderReader::Seek, aTarget,
                     aEndTime.ToMicroseconds());
}

void
MediaDecoderReaderWrapper::WaitForData(MediaData::Type aType)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());

  auto p = InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                       &MediaDecoderReader::WaitForData, aType);

  RefPtr<MediaDecoderReaderWrapper> self = this;
  WaitRequestRef(aType).Begin(p->Then(mOwnerThread, __func__,
    [self] (MediaData::Type aType) {
      self->WaitRequestRef(aType).Complete();
      self->WaitCallbackRef(aType).Notify(AsVariant(aType));
    },
    [self, aType] (WaitForDataRejectValue aRejection) {
      self->WaitRequestRef(aType).Complete();
      self->WaitCallbackRef(aType).Notify(AsVariant(aRejection));
    }));
}

MediaCallbackExc<WaitCallbackData>&
MediaDecoderReaderWrapper::WaitCallbackRef(MediaData::Type aType)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return aType == MediaData::AUDIO_DATA ? mAudioWaitCallback : mVideoWaitCallback;
}

MozPromiseRequestHolder<MediaDecoderReader::WaitForDataPromise>&
MediaDecoderReaderWrapper::WaitRequestRef(MediaData::Type aType)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return aType == MediaData::AUDIO_DATA ? mAudioWaitRequest : mVideoWaitRequest;
}

RefPtr<MediaDecoderReaderWrapper::BufferedUpdatePromise>
MediaDecoderReaderWrapper::UpdateBufferedWithPromise()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                     &MediaDecoderReader::UpdateBufferedWithPromise);
}

void
MediaDecoderReaderWrapper::ReleaseResources()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  nsCOMPtr<nsIRunnable> r =
    NewRunnableMethod(mReader, &MediaDecoderReader::ReleaseResources);
  mReader->OwnerThread()->Dispatch(r.forget());
}

void
MediaDecoderReaderWrapper::SetIdle()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  nsCOMPtr<nsIRunnable> r =
    NewRunnableMethod(mReader, &MediaDecoderReader::SetIdle);
  mReader->OwnerThread()->Dispatch(r.forget());
}

void
MediaDecoderReaderWrapper::ResetDecode(TrackSet aTracks)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());

  if (aTracks.contains(TrackInfo::kAudioTrack)) {
    mAudioDataRequest.DisconnectIfExists();
    mAudioWaitRequest.DisconnectIfExists();
  }

  if (aTracks.contains(TrackInfo::kVideoTrack)) {
    mVideoDataRequest.DisconnectIfExists();
    mVideoWaitRequest.DisconnectIfExists();
  }

  nsCOMPtr<nsIRunnable> r =
    NewRunnableMethod<TrackSet>(mReader,
                                &MediaDecoderReader::ResetDecode,
                                aTracks);
  mReader->OwnerThread()->Dispatch(r.forget());
}

RefPtr<ShutdownPromise>
MediaDecoderReaderWrapper::Shutdown()
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  MOZ_ASSERT(!mAudioDataRequest.Exists());
  MOZ_ASSERT(!mVideoDataRequest.Exists());

  mShutdown = true;
  if (mStartTimeRendezvous) {
    mStartTimeRendezvous->Destroy();
    mStartTimeRendezvous = nullptr;
  }
  return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
                     &MediaDecoderReader::Shutdown);
}

void
MediaDecoderReaderWrapper::OnMetadataRead(MetadataHolder* aMetadata)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  if (mShutdown) {
    return;
  }
  // Set up the start time rendezvous if it doesn't already exist (which is
  // generally the case, unless we're coming out of dormant mode).
  if (!mStartTimeRendezvous) {
    mStartTimeRendezvous = new StartTimeRendezvous(
      mOwnerThread, aMetadata->mInfo.HasAudio(),
      aMetadata->mInfo.HasVideo(), mForceZeroStartTime);

    RefPtr<MediaDecoderReaderWrapper> self = this;
    mStartTimeRendezvous->AwaitStartTime()->Then(
      mOwnerThread, __func__,
      [self] ()  {
        NS_ENSURE_TRUE_VOID(!self->mShutdown);
        self->mReader->DispatchSetStartTime(self->StartTime().ToMicroseconds());
      },
      [] () {
        NS_WARNING("Setting start time on reader failed");
      });
  }
}

void
MediaDecoderReaderWrapper::SetVideoBlankDecode(bool aIsBlankDecode)
{
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  nsCOMPtr<nsIRunnable> r =
    NewRunnableMethod<bool>(mReader, &MediaDecoderReader::SetVideoBlankDecode,
                            aIsBlankDecode);
  mReader->OwnerThread()->Dispatch(r.forget());
}

} // namespace mozilla