/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "AllocationPolicy.h"

#include "ImageContainer.h"
#include "MediaInfo.h"
#include "PDMFactory.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/SchedulerGroup.h"
#ifdef MOZ_WIDGET_ANDROID
#  include "mozilla/jni/Utils.h"
#endif

namespace mozilla {

using TrackType = TrackInfo::TrackType;

class AllocPolicyImpl::AutoDeallocToken : public Token {
 public:
  explicit AutoDeallocToken(const RefPtr<AllocPolicyImpl>& aPolicy)
      : mPolicy(aPolicy) {}

 private:
  ~AutoDeallocToken() { mPolicy->Dealloc(); }

  RefPtr<AllocPolicyImpl> mPolicy;
};

AllocPolicyImpl::AllocPolicyImpl(int aDecoderLimit)
    : mMaxDecoderLimit(aDecoderLimit),
      mMonitor("AllocPolicyImpl"),
      mDecoderLimit(aDecoderLimit) {}
AllocPolicyImpl::~AllocPolicyImpl() { RejectAll(); }

auto AllocPolicyImpl::Alloc() -> RefPtr<Promise> {
  ReentrantMonitorAutoEnter mon(mMonitor);
  // No decoder limit set.
  if (mDecoderLimit < 0) {
    return Promise::CreateAndResolve(new Token(), __func__);
  }

  RefPtr<PromisePrivate> p = new PromisePrivate(__func__);
  mPromises.push(p);
  ResolvePromise(mon);
  return p;
}

void AllocPolicyImpl::Dealloc() {
  ReentrantMonitorAutoEnter mon(mMonitor);
  ++mDecoderLimit;
  ResolvePromise(mon);
}

void AllocPolicyImpl::ResolvePromise(ReentrantMonitorAutoEnter& aProofOfLock) {
  MOZ_ASSERT(mDecoderLimit >= 0);

  if (mDecoderLimit > 0 && !mPromises.empty()) {
    --mDecoderLimit;
    RefPtr<PromisePrivate> p = std::move(mPromises.front());
    mPromises.pop();
    p->Resolve(new AutoDeallocToken(this), __func__);
  }
}

void AllocPolicyImpl::RejectAll() {
  ReentrantMonitorAutoEnter mon(mMonitor);
  while (!mPromises.empty()) {
    RefPtr<PromisePrivate> p = std::move(mPromises.front());
    mPromises.pop();
    p->Reject(true, __func__);
  }
}

static int32_t MediaDecoderLimitDefault() {
#ifdef MOZ_WIDGET_ANDROID
  if (jni::GetAPIVersion() < 18) {
    // Older Android versions have broken support for multiple simultaneous
    // decoders, see bug 1278574.
    return 1;
  }
#endif
  // Otherwise, set no decoder limit.
  return -1;
}

StaticMutex GlobalAllocPolicy::sMutex;

NotNull<AllocPolicy*> GlobalAllocPolicy::Instance(TrackType aTrack) {
  StaticMutexAutoLock lock(sMutex);
  if (aTrack == TrackType::kAudioTrack) {
    static RefPtr<AllocPolicyImpl> sAudioPolicy = []() {
      SchedulerGroup::Dispatch(
          TaskCategory::Other,
          NS_NewRunnableFunction(
              "GlobalAllocPolicy::GlobalAllocPolicy:Audio", []() {
                ClearOnShutdown(&sAudioPolicy,
                                ShutdownPhase::XPCOMShutdownThreads);
              }));
      return new AllocPolicyImpl(MediaDecoderLimitDefault());
    }();
    return WrapNotNull(sAudioPolicy.get());
  }
  static RefPtr<AllocPolicyImpl> sVideoPolicy = []() {
    SchedulerGroup::Dispatch(
        TaskCategory::Other,
        NS_NewRunnableFunction(
            "GlobalAllocPolicy::GlobalAllocPolicy:Audio", []() {
              ClearOnShutdown(&sVideoPolicy,
                              ShutdownPhase::XPCOMShutdownThreads);
            }));
    return new AllocPolicyImpl(MediaDecoderLimitDefault());
  }();
  return WrapNotNull(sVideoPolicy.get());
}

class SingleAllocPolicy::AutoDeallocCombinedToken : public Token {
 public:
  AutoDeallocCombinedToken(already_AddRefed<Token> aSingleAllocPolicyToken,
                           already_AddRefed<Token> aGlobalAllocPolicyToken)
      : mSingleToken(aSingleAllocPolicyToken),
        mGlobalToken(aGlobalAllocPolicyToken) {}

 private:
  // Release tokens allocated from GlobalAllocPolicy and LocalAllocPolicy
  // and process next token request if any.
  ~AutoDeallocCombinedToken() = default;
  const RefPtr<Token> mSingleToken;
  const RefPtr<Token> mGlobalToken;
};

auto SingleAllocPolicy::Alloc() -> RefPtr<Promise> {
  MOZ_DIAGNOSTIC_ASSERT(MaxDecoderLimit() == 1,
                        "We can only handle at most one token out at a time.");
  RefPtr<SingleAllocPolicy> self = this;
  return AllocPolicyImpl::Alloc()->Then(
      mOwnerThread, __func__,
      [self](RefPtr<Token> aToken) {
        RefPtr<Token> localToken = std::move(aToken);
        RefPtr<Promise> p = self->mPendingPromise.Ensure(__func__);
        GlobalAllocPolicy::Instance(self->mTrack)
            ->Alloc()
            ->Then(
                self->mOwnerThread, __func__,
                [self, localToken = std::move(localToken)](
                    RefPtr<Token> aToken) mutable {
                  self->mTokenRequest.Complete();
                  RefPtr<Token> combinedToken = new AutoDeallocCombinedToken(
                      localToken.forget(), aToken.forget());
                  self->mPendingPromise.Resolve(combinedToken, __func__);
                },
                [self]() {
                  self->mTokenRequest.Complete();
                  self->mPendingPromise.Reject(true, __func__);
                })
            ->Track(self->mTokenRequest);
        return p;
      },
      []() { return Promise::CreateAndReject(true, __func__); });
}

SingleAllocPolicy::~SingleAllocPolicy() {
  mPendingPromise.RejectIfExists(true, __func__);
  mTokenRequest.DisconnectIfExists();
}

void SingleAllocPolicy::Cancel() {
  MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
  mPendingPromise.RejectIfExists(true, __func__);
  mTokenRequest.DisconnectIfExists();
  RejectAll();
}

AllocationWrapper::AllocationWrapper(
    already_AddRefed<MediaDataDecoder> aDecoder, already_AddRefed<Token> aToken)
    : mDecoder(aDecoder), mToken(aToken) {
  DecoderDoctorLogger::LogConstructionAndBase(
      "AllocationWrapper", this, static_cast<const MediaDataDecoder*>(this));
  DecoderDoctorLogger::LinkParentAndChild("AllocationWrapper", this, "decoder",
                                          mDecoder.get());
}

AllocationWrapper::~AllocationWrapper() {
  DecoderDoctorLogger::LogDestruction("AllocationWrapper", this);
}

RefPtr<ShutdownPromise> AllocationWrapper::Shutdown() {
  RefPtr<MediaDataDecoder> decoder = std::move(mDecoder);
  RefPtr<Token> token = std::move(mToken);
  return decoder->Shutdown()->Then(
      GetCurrentSerialEventTarget(), __func__,
      [token]() { return ShutdownPromise::CreateAndResolve(true, __func__); });
}
/* static */ RefPtr<AllocationWrapper::AllocateDecoderPromise>
AllocationWrapper::CreateDecoder(const CreateDecoderParams& aParams,
                                 AllocPolicy* aPolicy) {
  RefPtr<AllocateDecoderPromise> p =
      (aPolicy ? aPolicy : GlobalAllocPolicy::Instance(aParams.mType))
          ->Alloc()
          ->Then(
              GetCurrentSerialEventTarget(), __func__,
              [params =
                   CreateDecoderParamsForAsync(aParams)](RefPtr<Token> aToken) {
                // result may not always be updated by
                // PDMFactory::CreateDecoder either when the creation
                // succeeded or failed, as such it must be initialized to a
                // fatal error by default.
                MediaResult result =
                    MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                nsPrintfCString("error creating %s decoder",
                                                TrackTypeToStr(params.mType)));
                RefPtr<PDMFactory> pdm = new PDMFactory();
                RefPtr<PlatformDecoderModule::CreateDecoderPromise> p =
                    pdm->CreateDecoder(params)->Then(
                        GetCurrentSerialEventTarget(), __func__,
                        [aToken](RefPtr<MediaDataDecoder>&& aDecoder) mutable {
                          RefPtr<AllocationWrapper> wrapper =
                              new AllocationWrapper(aDecoder.forget(),
                                                    aToken.forget());
                          return AllocateDecoderPromise::CreateAndResolve(
                              wrapper, __func__);
                        },
                        [](const MediaResult& aError) {
                          return AllocateDecoderPromise::CreateAndReject(
                              aError, __func__);
                        });
                return p;
              },
              []() {
                return AllocateDecoderPromise::CreateAndReject(
                    MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
                                "Allocation policy expired"),
                    __func__);
              });
  return p;
}

}  // namespace mozilla
