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

#include "mozilla/Monitor.h"
#include "nsContentUtils.h"
#include "nsIAsyncInputStream.h"
#include "nsNetCID.h"

namespace mozilla {

namespace {

class InputStreamReader final : public nsIInputStreamCallback
{
public:
  NS_DECL_THREADSAFE_ISUPPORTS

  static already_AddRefed<InputStreamReader>
  Create(nsICloneableInputStreamWithRange* aStream, int64_t aStart,
         uint32_t aLength)
  {
    MOZ_ASSERT(aStream);

    nsCOMPtr<nsIInputStream> stream;
    nsresult rv = aStream->CloneWithRange(aStart, aLength,
                                          getter_AddRefs(stream));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return nullptr;
    }

    RefPtr<InputStreamReader> reader = new InputStreamReader(stream);
    return reader.forget();
  }

  nsresult
  Read(char* aBuffer, uint32_t aSize, uint32_t* aRead)
  {
    uint32_t done = 0;
    do {
      uint32_t read;
      nsresult rv = SyncRead(aBuffer + done, aSize - done, &read);
      if (NS_SUCCEEDED(rv) && read == 0) {
        break;
      }
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      done += read;
    } while(done != aSize);

    *aRead = done;
    return NS_OK;
  }

  NS_IMETHOD
  OnInputStreamReady(nsIAsyncInputStream* aStream) override
  {
    // Let's continue with SyncRead().
    MonitorAutoLock lock(mMonitor);
    return lock.Notify();
  }

private:
  explicit InputStreamReader(nsIInputStream* aStream)
    : mStream(aStream)
    , mMonitor("InputStreamReader::mMonitor")
  {
    MOZ_ASSERT(aStream);
  }

  ~InputStreamReader() = default;

  nsresult
  SyncRead(char* aBuffer, uint32_t aSize, uint32_t* aRead)
  {
    while (1) {
      nsresult rv = mStream->Read(aBuffer, aSize, aRead);
      // All good.
      if (rv == NS_BASE_STREAM_CLOSED || NS_SUCCEEDED(rv)) {
        return NS_OK;
      }

      // An error.
      if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
        return rv;
      }

      // We need to proceed async.
      if (!mAsyncStream) {
        mAsyncStream = do_QueryInterface(mStream);
      }

      if (!mAsyncStream) {
        return NS_ERROR_FAILURE;
      }

      nsCOMPtr<nsIEventTarget> target =
        do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
      MOZ_ASSERT(target);

      {
        // We wait for ::OnInputStreamReady() to be called.
        MonitorAutoLock lock(mMonitor);

        rv = mAsyncStream->AsyncWait(this, 0, aSize, target);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }

        lock.Wait();
      }
    }
  }

  nsCOMPtr<nsIInputStream> mStream;
  nsCOMPtr<nsIAsyncInputStream> mAsyncStream;
  Monitor mMonitor;
};

NS_IMPL_ADDREF(InputStreamReader);
NS_IMPL_RELEASE(InputStreamReader);

NS_INTERFACE_MAP_BEGIN(InputStreamReader)
  NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback)
NS_INTERFACE_MAP_END

} // anonymous

void
CloneableWithRangeMediaResource::MaybeInitialize()
{
  if (!mInitialized) {
    mInitialized = true;
    mCallback->NotifyDataEnded(NS_OK);
  }
}

nsresult
CloneableWithRangeMediaResource::GetCachedRanges(MediaByteRangeSet& aRanges)
{
  MaybeInitialize();
  aRanges += MediaByteRange(0, (int64_t)mSize);
  return NS_OK;
}

nsresult
CloneableWithRangeMediaResource::Open(nsIStreamListener** aStreamListener)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aStreamListener);

  *aStreamListener = nullptr;
  return NS_OK;
}

nsresult
CloneableWithRangeMediaResource::Close()
{
  return NS_OK;
}

already_AddRefed<nsIPrincipal>
CloneableWithRangeMediaResource::GetCurrentPrincipal()
{
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIPrincipal> principal;
  nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
  if (!secMan || !mChannel) {
    return nullptr;
  }

  secMan->GetChannelResultPrincipal(mChannel, getter_AddRefs(principal));
  return principal.forget();
}

nsresult
CloneableWithRangeMediaResource::ReadFromCache(char* aBuffer, int64_t aOffset,
                                               uint32_t aCount)
{
  MaybeInitialize();
  if (!aCount) {
    return NS_OK;
  }

  RefPtr<InputStreamReader> reader =
    InputStreamReader::Create(mStream, aOffset, aCount);
  if (!reader) {
    return NS_ERROR_FAILURE;
  }

  uint32_t bytes = 0;
  nsresult rv = reader->Read(aBuffer, aCount, &bytes);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mCurrentPosition = bytes + aOffset;
  return bytes == aCount ? NS_OK : NS_ERROR_FAILURE;
}

nsresult
CloneableWithRangeMediaResource::ReadAt(int64_t aOffset, char* aBuffer,
                                        uint32_t aCount, uint32_t* aBytes)
{
  MOZ_ASSERT(!NS_IsMainThread());

  RefPtr<InputStreamReader> reader =
    InputStreamReader::Create(mStream, aOffset, aCount);
  if (!reader) {
    return NS_ERROR_FAILURE;
  }

  nsresult rv = reader->Read(aBuffer, aCount, aBytes);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mCurrentPosition = *aBytes + aOffset;
  return NS_OK;
}

int64_t CloneableWithRangeMediaResource::Tell()
{
  MaybeInitialize();
  return mCurrentPosition;
}

} // mozilla namespace
