/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "NextFrameSeekTask.h"
#include "MediaDecoderReaderWrapper.h"
#include "mozilla/AbstractThread.h"
#include "mozilla/Assertions.h"
#include "nsPrintfCString.h"

namespace mozilla {

extern LazyLogModule gMediaSampleLog;

#define SAMPLE_LOG(x, ...) MOZ_LOG(gMediaSampleLog, LogLevel::Debug, \
  ("[NextFrameSeekTask] Decoder=%p " x, mDecoderID, ##__VA_ARGS__))

namespace media {

NextFrameSeekTask::NextFrameSeekTask(const void* aDecoderID,
                                     AbstractThread* aThread,
                                     MediaDecoderReaderWrapper* aReader,
                                     const SeekTarget& aTarget,
                                     const MediaInfo& aInfo,
                                     const media::TimeUnit& aDuration,
                                     int64_t aCurrentTime,
                                     MediaQueue<MediaData>& aAudioQueue,
                                     MediaQueue<MediaData>& aVideoQueue)
  : SeekTask(aDecoderID, aThread, aReader, aTarget)
  , mAudioQueue(aAudioQueue)
  , mVideoQueue(aVideoQueue)
  , mCurrentTime(aCurrentTime)
  , mDuration(aDuration)
{
  AssertOwnerThread();
  MOZ_ASSERT(aInfo.HasVideo());

  // Configure MediaDecoderReaderWrapper.
  SetCallbacks();
}

NextFrameSeekTask::~NextFrameSeekTask()
{
  AssertOwnerThread();
  MOZ_ASSERT(mIsDiscarded);
}

void
NextFrameSeekTask::Discard()
{
  AssertOwnerThread();

  // Disconnect MDSM.
  RejectIfExist(NS_ERROR_DOM_MEDIA_CANCELED, __func__);

  // Disconnect MediaDecoderReader.
  CancelCallbacks();

  mIsDiscarded = true;
}

bool
NextFrameSeekTask::NeedToResetMDSM() const
{
  AssertOwnerThread();
  return false;
}

/*
 * Remove samples from the queue until aCompare() returns false.
 * aCompare A function object with the signature bool(int64_t) which returns
 *          true for samples that should be removed.
 */
template <typename Function> static void
DiscardFrames(MediaQueue<MediaData>& aQueue, const Function& aCompare)
{
  while(aQueue.GetSize() > 0) {
    if (aCompare(aQueue.PeekFront()->mTime)) {
      RefPtr<MediaData> releaseMe = aQueue.PopFront();
      continue;
    }
    break;
  }
}

RefPtr<NextFrameSeekTask::SeekTaskPromise>
NextFrameSeekTask::Seek(const media::TimeUnit&)
{
  AssertOwnerThread();

  auto currentTime = mCurrentTime;
  DiscardFrames(mVideoQueue, [currentTime] (int64_t aSampleTime) {
    return aSampleTime <= currentTime;
  });

  RefPtr<SeekTaskPromise> promise = mSeekTaskPromise.Ensure(__func__);
  if (!IsVideoRequestPending() && NeedMoreVideo()) {
    RequestVideoData();
  }
  MaybeFinishSeek(); // Might resolve mSeekTaskPromise and modify audio queue.
  return promise;
}

void
NextFrameSeekTask::RequestVideoData()
{
  AssertOwnerThread();
  mReader->RequestVideoData(false, media::TimeUnit());
}

bool
NextFrameSeekTask::NeedMoreVideo() const
{
  AssertOwnerThread();
  // Need to request video when we have none and video queue is not finished.
  return mVideoQueue.GetSize() == 0 &&
         !mSeekedVideoData &&
         !mVideoQueue.IsFinished() &&
         !mIsVideoQueueFinished;
}

bool
NextFrameSeekTask::IsVideoRequestPending() const
{
  AssertOwnerThread();
  return mReader->IsRequestingVideoData() || mReader->IsWaitingVideoData();
}

bool
NextFrameSeekTask::IsAudioSeekComplete() const
{
  AssertOwnerThread();
  // Don't finish seek until there are no pending requests. Otherwise, we might
  // lose audio samples for the promise is resolved asynchronously.
  return !mReader->IsRequestingAudioData() && !mReader->IsWaitingAudioData();
}

bool
NextFrameSeekTask::IsVideoSeekComplete() const
{
  AssertOwnerThread();
  // Don't finish seek until there are no pending requests. Otherwise, we might
  // lose video samples for the promise is resolved asynchronously.
  return !IsVideoRequestPending() && !NeedMoreVideo();
}

void
NextFrameSeekTask::MaybeFinishSeek()
{
  AssertOwnerThread();
  if (IsAudioSeekComplete() && IsVideoSeekComplete()) {
    UpdateSeekTargetTime();

    auto time = mTarget.GetTime().ToMicroseconds();
    DiscardFrames(mAudioQueue, [time] (int64_t aSampleTime) {
      return aSampleTime < time;
    });

    Resolve(__func__); // Call to MDSM::SeekCompleted();
  }
}

void
NextFrameSeekTask::OnAudioDecoded(MediaData* aAudioSample)
{
  AssertOwnerThread();
  MOZ_ASSERT(aAudioSample);
  MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");

  // The MDSM::mDecodedAudioEndTime will be updated once the whole SeekTask is
  // resolved.

  SAMPLE_LOG("OnAudioDecoded [%lld,%lld]",
             aAudioSample->mTime,
             aAudioSample->GetEndTime());

  // We accept any audio data here.
  mSeekedAudioData = aAudioSample;

  MaybeFinishSeek();
}

void
NextFrameSeekTask::OnAudioNotDecoded(const MediaResult& aError)
{
  AssertOwnerThread();
  MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");

  SAMPLE_LOG("OnAudioNotDecoded (aError=%u)", aError.Code());

  // We don't really handle audio deocde error here. Let MDSM to trigger further
  // audio decoding tasks if it needs to play audio, and MDSM will then receive
  // the decoding state from MediaDecoderReader.

  MaybeFinishSeek();
}

void
NextFrameSeekTask::OnVideoDecoded(MediaData* aVideoSample)
{
  AssertOwnerThread();
  MOZ_ASSERT(aVideoSample);
  MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");

  // The MDSM::mDecodedVideoEndTime will be updated once the whole SeekTask is
  // resolved.

  SAMPLE_LOG("OnVideoDecoded [%lld,%lld]",
             aVideoSample->mTime,
             aVideoSample->GetEndTime());

  if (aVideoSample->mTime > mCurrentTime) {
    mSeekedVideoData = aVideoSample;
  }

  if (NeedMoreVideo()) {
    RequestVideoData();
    return;
  }

  MaybeFinishSeek();
}

void
NextFrameSeekTask::OnVideoNotDecoded(const MediaResult& aError)
{
  AssertOwnerThread();
  MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");

  SAMPLE_LOG("OnVideoNotDecoded (aError=%u)", aError.Code());

  if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
    mIsVideoQueueFinished = true;
  }

  // Video seek not finished.
  if (NeedMoreVideo()) {
    switch (aError.Code()) {
      case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
        mReader->WaitForData(MediaData::VIDEO_DATA);
        break;
      case NS_ERROR_DOM_MEDIA_CANCELED:
        RequestVideoData();
        break;
      case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
        MOZ_ASSERT(false, "Shouldn't want more data for ended video.");
        break;
      default:
        // We might lose the audio sample after canceling the callbacks.
        // However it doesn't really matter because MDSM is gonna shut down
        // when seek fails.
        CancelCallbacks();
        // Reject the promise since we can't finish video seek anyway.
        RejectIfExist(aError, __func__);
        break;
    }
    return;
  }

  MaybeFinishSeek();
}

void
NextFrameSeekTask::SetCallbacks()
{
  AssertOwnerThread();

  // Register dummy callbcak for audio decoding since we don't need to handle
  // the decoded audio samples.
  RefPtr<NextFrameSeekTask> self = this;
  mAudioCallback = mReader->AudioCallback().Connect(
    OwnerThread(), [self] (AudioCallbackData aData) {
    if (aData.is<MediaData*>()) {
      self->OnAudioDecoded(aData.as<MediaData*>());
    } else {
      self->OnAudioNotDecoded(aData.as<MediaResult>());
    }
  });

  mVideoCallback = mReader->VideoCallback().Connect(
    OwnerThread(), [self] (VideoCallbackData aData) {
    typedef Tuple<MediaData*, TimeStamp> Type;
    if (aData.is<Type>()) {
      self->OnVideoDecoded(Get<0>(aData.as<Type>()));
    } else {
      self->OnVideoNotDecoded(aData.as<MediaResult>());
    }
  });

  mAudioWaitCallback = mReader->AudioWaitCallback().Connect(
    OwnerThread(), [self] (WaitCallbackData aData) {
    // We don't make an audio decode request here, instead, let MDSM to
    // trigger further audio decode tasks if MDSM itself needs to play audio.
    self->MaybeFinishSeek();
  });

  mVideoWaitCallback = mReader->VideoWaitCallback().Connect(
    OwnerThread(), [self] (WaitCallbackData aData) {
    if (self->NeedMoreVideo()) {
      if (aData.is<MediaData::Type>()) {
        self->RequestVideoData();
      } else {
        // Reject if we can't finish video seeking.
        self->CancelCallbacks();
        self->RejectIfExist(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
      }
      return;
    }
    self->MaybeFinishSeek();
  });
}

void
NextFrameSeekTask::CancelCallbacks()
{
  AssertOwnerThread();
  mAudioCallback.DisconnectIfExists();
  mVideoCallback.DisconnectIfExists();
  mAudioWaitCallback.DisconnectIfExists();
  mVideoWaitCallback.DisconnectIfExists();
}

void
NextFrameSeekTask::UpdateSeekTargetTime()
{
  AssertOwnerThread();

  RefPtr<MediaData> data = mVideoQueue.PeekFront();
  if (data) {
    mTarget.SetTime(TimeUnit::FromMicroseconds(data->mTime));
  } else if (mSeekedVideoData) {
    mTarget.SetTime(TimeUnit::FromMicroseconds(mSeekedVideoData->mTime));
  } else if (mIsVideoQueueFinished || mVideoQueue.AtEndOfStream()) {
    mTarget.SetTime(mDuration);
  } else {
    MOZ_ASSERT(false, "No data!");
  }
}
} // namespace media
} // namespace mozilla
