/* -*- 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/CDMProxy.h"
#include "nsString.h"
#include "mozilla/dom/MediaKeys.h"
#include "mozilla/dom/MediaKeySession.h"
#include "mozIGeckoMediaPluginService.h"
#include "nsContentCID.h"
#include "nsServiceManagerUtils.h"
#include "MainThreadUtils.h"
#include "mozilla/EMELog.h"
#include "nsIConsoleService.h"
#include "prenv.h"
#include "mozilla/PodOperations.h"
#include "mozilla/CDMCallbackProxy.h"

namespace mozilla {

CDMProxy::CDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem)
  : mKeys(aKeys)
  , mKeySystem(aKeySystem)
  , mCDM(nullptr)
  , mDecryptionJobCount(0)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_COUNT_CTOR(CDMProxy);
}

CDMProxy::~CDMProxy()
{
  MOZ_COUNT_DTOR(CDMProxy);
}

void
CDMProxy::Init(PromiseId aPromiseId)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_ENSURE_TRUE_VOID(!mKeys.IsNull());

  nsresult rv = mKeys->GetOrigin(mOrigin);
  if (NS_FAILED(rv)) {
    RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  EME_LOG("Creating CDMProxy for origin='%s'",
          NS_ConvertUTF16toUTF8(GetOrigin()).get());

  if (!mGMPThread) {
    nsCOMPtr<mozIGeckoMediaPluginService> mps =
      do_GetService("@mozilla.org/gecko-media-plugin-service;1");
    if (!mps) {
      RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
      return;
    }
    mps->GetThread(getter_AddRefs(mGMPThread));
    if (!mGMPThread) {
      RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
      return;
    }
  }

  nsRefPtr<nsIRunnable> task(NS_NewRunnableMethodWithArg<uint32_t>(this, &CDMProxy::gmp_Init, aPromiseId));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

#ifdef DEBUG
bool
CDMProxy::IsOnGMPThread()
{
  return NS_GetCurrentThread() == mGMPThread;
}
#endif

void
CDMProxy::gmp_Init(uint32_t aPromiseId)
{
  MOZ_ASSERT(IsOnGMPThread());

  nsCOMPtr<mozIGeckoMediaPluginService> mps =
    do_GetService("@mozilla.org/gecko-media-plugin-service;1");
  if (!mps) {
    RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }

  nsTArray<nsCString> tags;
  tags.AppendElement(NS_ConvertUTF16toUTF8(mKeySystem));
  nsresult rv = mps->GetGMPDecryptor(&tags, GetOrigin(), &mCDM);
  if (NS_FAILED(rv) || !mCDM) {
    RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
  } else {
    mCallback = new CDMCallbackProxy(this);
    mCDM->Init(mCallback);
    nsRefPtr<nsIRunnable> task(NS_NewRunnableMethodWithArg<uint32_t>(this, &CDMProxy::OnCDMCreated, aPromiseId));
    NS_DispatchToMainThread(task);
  }
}

void
CDMProxy::OnCDMCreated(uint32_t aPromiseId)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (mKeys.IsNull()) {
    return;
  }
  mKeys->OnCDMCreated(aPromiseId);
}

void
CDMProxy::CreateSession(dom::SessionType aSessionType,
                        PromiseId aPromiseId,
                        const nsAString& aInitDataType,
                        nsTArray<uint8_t>& aInitData)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mGMPThread);

  nsAutoPtr<CreateSessionData> data(new CreateSessionData());
  data->mSessionType = aSessionType;
  data->mPromiseId = aPromiseId;
  data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType);
  data->mInitData = Move(aInitData);

  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<CreateSessionData>>(this, &CDMProxy::gmp_CreateSession, data));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

GMPSessionType
ToGMPSessionType(dom::SessionType aSessionType) {
  switch (aSessionType) {
    case dom::SessionType::Temporary: return kGMPTemporySession;
    case dom::SessionType::Persistent: return kGMPPersistentSession;
    default: return kGMPTemporySession;
  };
};

void
CDMProxy::gmp_CreateSession(nsAutoPtr<CreateSessionData> aData)
{
  MOZ_ASSERT(IsOnGMPThread());
  if (!mCDM) {
    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mCDM->CreateSession(aData->mPromiseId,
                      aData->mInitDataType,
                      aData->mInitData,
                      ToGMPSessionType(aData->mSessionType));
}

void
CDMProxy::LoadSession(PromiseId aPromiseId,
                      const nsAString& aSessionId)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mGMPThread);

  nsAutoPtr<SessionOpData> data(new SessionOpData());
  data->mPromiseId = aPromiseId;
  data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<SessionOpData>>(this, &CDMProxy::gmp_LoadSession, data));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_LoadSession(nsAutoPtr<SessionOpData> aData)
{
  MOZ_ASSERT(IsOnGMPThread());

  if (!mCDM) {
    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mCDM->LoadSession(aData->mPromiseId, aData->mSessionId);
}

void
CDMProxy::SetServerCertificate(PromiseId aPromiseId,
                               nsTArray<uint8_t>& aCert)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mGMPThread);

  nsAutoPtr<SetServerCertificateData> data;
  data->mPromiseId = aPromiseId;
  data->mCert = Move(aCert);
  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<SetServerCertificateData>>(this, &CDMProxy::gmp_SetServerCertificate, data));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_SetServerCertificate(nsAutoPtr<SetServerCertificateData> aData)
{
  MOZ_ASSERT(IsOnGMPThread());
  if (!mCDM) {
    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mCDM->SetServerCertificate(aData->mPromiseId, aData->mCert);
}

void
CDMProxy::UpdateSession(const nsAString& aSessionId,
                        PromiseId aPromiseId,
                        nsTArray<uint8_t>& aResponse)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mGMPThread);
  NS_ENSURE_TRUE_VOID(!mKeys.IsNull());

  nsAutoPtr<UpdateSessionData> data(new UpdateSessionData());
  data->mPromiseId = aPromiseId;
  data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
  data->mResponse = Move(aResponse);
  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<UpdateSessionData>>(this, &CDMProxy::gmp_UpdateSession, data));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_UpdateSession(nsAutoPtr<UpdateSessionData> aData)
{
  MOZ_ASSERT(IsOnGMPThread());
  if (!mCDM) {
    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mCDM->UpdateSession(aData->mPromiseId,
                      aData->mSessionId,
                      aData->mResponse);
}

void
CDMProxy::CloseSession(const nsAString& aSessionId,
                       PromiseId aPromiseId)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_ENSURE_TRUE_VOID(!mKeys.IsNull());

  {
    CDMCaps::AutoLock caps(Capabilites());
    caps.DropKeysForSession(aSessionId);
  }
  nsAutoPtr<SessionOpData> data(new SessionOpData());
  data->mPromiseId = aPromiseId;
  data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<SessionOpData>>(this, &CDMProxy::gmp_CloseSession, data));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_CloseSession(nsAutoPtr<SessionOpData> aData)
{
  MOZ_ASSERT(IsOnGMPThread());
  if (!mCDM) {
    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mCDM->CloseSession(aData->mPromiseId, aData->mSessionId);
}

void
CDMProxy::RemoveSession(const nsAString& aSessionId,
                        PromiseId aPromiseId)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_ENSURE_TRUE_VOID(!mKeys.IsNull());

  {
    CDMCaps::AutoLock caps(Capabilites());
    caps.DropKeysForSession(aSessionId);
  }
  nsAutoPtr<SessionOpData> data(new SessionOpData());
  data->mPromiseId = aPromiseId;
  data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<SessionOpData>>(this, &CDMProxy::gmp_RemoveSession, data));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_RemoveSession(nsAutoPtr<SessionOpData> aData)
{
  MOZ_ASSERT(IsOnGMPThread());
  if (!mCDM) {
    RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mCDM->RemoveSession(aData->mPromiseId, aData->mSessionId);
}

void
CDMProxy::Shutdown()
{
  MOZ_ASSERT(NS_IsMainThread());
  mKeys.Clear();
  // Note: This may end up being the last owning reference to the CDMProxy.
  nsRefPtr<nsIRunnable> task(NS_NewRunnableMethod(this, &CDMProxy::gmp_Shutdown));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_Shutdown()
{
  MOZ_ASSERT(IsOnGMPThread());

  // Abort any pending decrypt jobs, to awaken any clients waiting on a job.
  for (size_t i = 0; i < mDecryptionJobs.Length(); i++) {
    DecryptJob* job = mDecryptionJobs[i];
    job->mClient->Decrypted(NS_ERROR_ABORT, nullptr);
  }
  mDecryptionJobs.Clear();

  if (mCDM) {
    mCDM->Close();
    mCDM = nullptr;
  }
}

void
CDMProxy::RejectPromise(PromiseId aId, nsresult aCode)
{
  if (NS_IsMainThread()) {
    if (!mKeys.IsNull()) {
      mKeys->RejectPromise(aId, aCode);
    }
  } else {
    nsRefPtr<nsIRunnable> task(new RejectPromiseTask(this, aId, aCode));
    NS_DispatchToMainThread(task);
  }
}

void
CDMProxy::ResolvePromise(PromiseId aId)
{
  if (NS_IsMainThread()) {
    if (!mKeys.IsNull()) {
      mKeys->ResolvePromise(aId);
    } else {
      NS_WARNING("CDMProxy unable to resolve promise!");
    }
  } else {
    nsRefPtr<nsIRunnable> task;
    task = NS_NewRunnableMethodWithArg<PromiseId>(this,
                                                  &CDMProxy::ResolvePromise,
                                                  aId);
    NS_DispatchToMainThread(task);
  }
}

const nsAString&
CDMProxy::GetOrigin() const
{
  return mOrigin;
}

void
CDMProxy::OnResolveNewSessionPromise(uint32_t aPromiseId,
                                     const nsAString& aSessionId)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (mKeys.IsNull()) {
    return;
  }
  mKeys->OnSessionCreated(aPromiseId, aSessionId);
}

void
CDMProxy::OnSessionMessage(const nsAString& aSessionId,
                           nsTArray<uint8_t>& aMessage,
                           const nsAString& aDestinationURL)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (mKeys.IsNull()) {
    return;
  }
  nsRefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
  if (session) {
    session->DispatchKeyMessage(aMessage, aDestinationURL);
  }
}

void
CDMProxy::OnExpirationChange(const nsAString& aSessionId,
                             GMPTimestamp aExpiryTime)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_WARNING("CDMProxy::OnExpirationChange() not implemented");
}

void
CDMProxy::OnSessionClosed(const nsAString& aSessionId)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_WARNING("CDMProxy::OnSessionClosed() not implemented");
}

static void
LogToConsole(const nsAString& aMsg)
{
  nsCOMPtr<nsIConsoleService> console(
    do_GetService("@mozilla.org/consoleservice;1"));
  if (!console) {
    NS_WARNING("Failed to log message to console.");
    return;
  }
  nsAutoString msg(aMsg);
  console->LogStringMessage(msg.get());
}

void
CDMProxy::OnSessionError(const nsAString& aSessionId,
                         nsresult aException,
                         uint32_t aSystemCode,
                         const nsAString& aMsg)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (mKeys.IsNull()) {
    return;
  }
  nsRefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
  if (session) {
    session->DispatchKeyError(aSystemCode);
  }
  LogToConsole(aMsg);
}

void
CDMProxy::OnRejectPromise(uint32_t aPromiseId,
                          nsresult aDOMException,
                          const nsAString& aMsg)
{
  MOZ_ASSERT(NS_IsMainThread());
  RejectPromise(aPromiseId, aDOMException);
  LogToConsole(aMsg);
}

const nsString&
CDMProxy::KeySystem() const
{
  return mKeySystem;
}

CDMCaps&
CDMProxy::Capabilites() {
  return mCapabilites;
}

void
CDMProxy::Decrypt(mp4_demuxer::MP4Sample* aSample,
                  DecryptionClient* aClient)
{
  nsAutoPtr<DecryptJob> job(new DecryptJob(aSample, aClient));
  nsRefPtr<nsIRunnable> task(
    NS_NewRunnableMethodWithArg<nsAutoPtr<DecryptJob>>(this, &CDMProxy::gmp_Decrypt, job));
  mGMPThread->Dispatch(task, NS_DISPATCH_NORMAL);
}

void
CDMProxy::gmp_Decrypt(nsAutoPtr<DecryptJob> aJob)
{
  MOZ_ASSERT(IsOnGMPThread());
  MOZ_ASSERT(aJob->mClient);
  MOZ_ASSERT(aJob->mSample);

  if (!mCDM) {
    aJob->mClient->Decrypted(NS_ERROR_FAILURE, nullptr);
    return;
  }

  aJob->mId = ++mDecryptionJobCount;
  nsTArray<uint8_t> data;
  data.AppendElements(aJob->mSample->data, aJob->mSample->size);
  mCDM->Decrypt(aJob->mId, aJob->mSample->crypto, data);
  mDecryptionJobs.AppendElement(aJob.forget());
}

void
CDMProxy::gmp_Decrypted(uint32_t aId,
                        GMPErr aResult,
                        const nsTArray<uint8_t>& aDecryptedData)
{
  MOZ_ASSERT(IsOnGMPThread());
  for (size_t i = 0; i < mDecryptionJobs.Length(); i++) {
    DecryptJob* job = mDecryptionJobs[i];
    if (job->mId == aId) {
      if (aDecryptedData.Length() != job->mSample->size) {
        NS_WARNING("CDM returned incorrect number of decrypted bytes");
      }
      if (GMP_SUCCEEDED(aResult)) {
        PodCopy(job->mSample->data,
                aDecryptedData.Elements(),
                std::min<size_t>(aDecryptedData.Length(), job->mSample->size));
        job->mClient->Decrypted(NS_OK, job->mSample.forget());
      } else {
        job->mClient->Decrypted(NS_ERROR_FAILURE, nullptr);
      }
      mDecryptionJobs.RemoveElementAt(i);
      return;
    } else {
      NS_WARNING("GMPDecryptorChild returned incorrect job ID");
    }
  }
}

void
CDMProxy::gmp_Terminated()
{
  MOZ_ASSERT(IsOnGMPThread());
  EME_LOG("CDM terminated");
  gmp_Shutdown();
}

} // namespace mozilla
