/* -*- Mode: C++; tab-width: 8; 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 "base/basictypes.h"
#include "AndroidCaptureProvider.h"
#include "nsXULAppAPI.h"
#include "AndroidBridge.h"
#include "nsStreamUtils.h"
#include "nsThreadUtils.h"
#include "nsMemory.h"
#include "RawStructs.h"

// The maximum number of frames we keep in our queue. Don't live in the past.
#define MAX_FRAMES_QUEUED 10

using namespace mozilla::net;

NS_IMPL_THREADSAFE_ISUPPORTS2(AndroidCameraInputStream, nsIInputStream, nsIAsyncInputStream)

AndroidCameraInputStream::AndroidCameraInputStream() :
  mWidth(0), mHeight(0), mCamera(0), mHeaderSent(false), mClosed(true), mFrameSize(0),
  mMonitor("AndroidCamera.Monitor")
{
  mAvailable = sizeof(RawVideoHeader);
  mFrameQueue = new nsDeque();
}

AndroidCameraInputStream::~AndroidCameraInputStream() {
  // clear the frame queue
  while (mFrameQueue->GetSize() > 0) {
    nsMemory::Free(mFrameQueue->PopFront());
  }
  delete mFrameQueue;
}

NS_IMETHODIMP
AndroidCameraInputStream::Init(nsACString& aContentType, nsCaptureParams* aParams)
{
  if (XRE_GetProcessType() != GeckoProcessType_Default)
    return NS_ERROR_NOT_IMPLEMENTED;

  mContentType = aContentType;
  mWidth = aParams->width;
  mHeight = aParams->height;
  mCamera = aParams->camera;
  
  CameraStreamImpl *impl = CameraStreamImpl::GetInstance(0);
  if (!impl)
    return NS_ERROR_OUT_OF_MEMORY;
  if (impl->Init(mContentType, mCamera, mWidth, mHeight, this)) {
    mWidth = impl->GetWidth();
    mHeight = impl->GetHeight();
    mClosed = false;
  }
  return NS_OK;
}

void AndroidCameraInputStream::ReceiveFrame(char* frame, uint32_t length) {
  {
    mozilla::ReentrantMonitorAutoEnter autoMonitor(mMonitor);
    if (mFrameQueue->GetSize() > MAX_FRAMES_QUEUED) {
      nsMemory::Free(mFrameQueue->PopFront());
      mAvailable -= mFrameSize;
    }
  }
  
  mFrameSize = sizeof(RawPacketHeader) + length;
  
  char* fullFrame = (char*)nsMemory::Alloc(mFrameSize);

  if (!fullFrame)
    return;
  
  RawPacketHeader* header = (RawPacketHeader*) fullFrame;
  header->packetID = 0xFF;
  header->codecID = 0x595556; // "YUV"
  
  // we copy the Y plane, and de-interlace the CrCb
  
  uint32_t yFrameSize = mWidth * mHeight;
  uint32_t uvFrameSize = yFrameSize / 4;

  memcpy(fullFrame + sizeof(RawPacketHeader), frame, yFrameSize);
  
  char* uFrame = fullFrame + yFrameSize;
  char* vFrame = fullFrame + yFrameSize + uvFrameSize;
  char* yFrame = frame + yFrameSize;
  for (uint32_t i = 0; i < uvFrameSize; i++) {
    uFrame[i] = yFrame[2 * i + 1];
    vFrame[i] = yFrame[2 * i];
  }
  
  {
    mozilla::ReentrantMonitorAutoEnter autoMonitor(mMonitor);
    mAvailable += mFrameSize;
    mFrameQueue->Push((void*)fullFrame);
  }

  NotifyListeners();
}

NS_IMETHODIMP
AndroidCameraInputStream::Available(uint64_t *aAvailable)
{
  mozilla::ReentrantMonitorAutoEnter autoMonitor(mMonitor);

  *aAvailable = mAvailable;

  return NS_OK;
}

NS_IMETHODIMP AndroidCameraInputStream::IsNonBlocking(bool *aNonBlock) {
  *aNonBlock = true;
  return NS_OK;
}

NS_IMETHODIMP AndroidCameraInputStream::Read(char *aBuffer, uint32_t aCount, uint32_t *aRead) {
  return ReadSegments(NS_CopySegmentToBuffer, aBuffer, aCount, aRead);
}

NS_IMETHODIMP AndroidCameraInputStream::ReadSegments(nsWriteSegmentFun aWriter, void *aClosure, uint32_t aCount, uint32_t *aRead) {
  *aRead = 0;
  
  nsresult rv;

  if (mAvailable == 0)
    return NS_BASE_STREAM_WOULD_BLOCK;
  
  if (aCount > mAvailable)
    aCount = mAvailable;

  if (!mHeaderSent) {
    CameraStreamImpl *impl = CameraStreamImpl::GetInstance(0);
    RawVideoHeader header;
    header.headerPacketID = 0;
    header.codecID = 0x595556; // "YUV"
    header.majorVersion = 0;
    header.minorVersion = 1;
    header.options = 1 | 1 << 1; // color, 4:2:2

    header.alphaChannelBpp = 0;
    header.lumaChannelBpp = 8;
    header.chromaChannelBpp = 4;
    header.colorspace = 1;

    header.frameWidth = mWidth;
    header.frameHeight = mHeight;
    header.aspectNumerator = 1;
    header.aspectDenominator = 1;

    header.framerateNumerator = impl->GetFps();
    header.framerateDenominator = 1;

    rv = aWriter(this, aClosure, (const char*)&header, 0, sizeof(RawVideoHeader), aRead);
   
    if (NS_FAILED(rv))
      return NS_OK;
    
    mHeaderSent = true;
    aCount -= sizeof(RawVideoHeader);
    mAvailable -= sizeof(RawVideoHeader);
  }
  
  {
    mozilla::ReentrantMonitorAutoEnter autoMonitor(mMonitor);
    while ((mAvailable > 0) && (aCount >= mFrameSize)) {
      uint32_t readThisTime = 0;

      char* frame = (char*)mFrameQueue->PopFront();
      rv = aWriter(this, aClosure, (const char*)frame, *aRead, mFrameSize, &readThisTime);

      if (readThisTime != mFrameSize) {
        mFrameQueue->PushFront((void*)frame);
        return NS_OK;
      }
  
      // RawReader does a copy when calling VideoData::Create()
      nsMemory::Free(frame);
  
      if (NS_FAILED(rv))
        return NS_OK;

      aCount -= readThisTime;
      mAvailable -= readThisTime;
      *aRead += readThisTime;
    }
  }
  return NS_OK;
}

NS_IMETHODIMP AndroidCameraInputStream::Close() {
  return CloseWithStatus(NS_OK);
}


/**
 * must be called on the main (java) thread
 */
void AndroidCameraInputStream::doClose() {
  NS_ASSERTION(!mClosed, "Camera is already closed");

  CameraStreamImpl *impl = CameraStreamImpl::GetInstance(0);
  impl->Close();
  mClosed = true;
}


void AndroidCameraInputStream::NotifyListeners() {
  mozilla::ReentrantMonitorAutoEnter autoMonitor(mMonitor);
  
  if (mCallback && (mAvailable > sizeof(RawVideoHeader))) {
    nsCOMPtr<nsIInputStreamCallback> callback;
    if (mCallbackTarget) {
      callback = NS_NewInputStreamReadyEvent(mCallback, mCallbackTarget);
    } else {
      callback = mCallback;
    }

    NS_ASSERTION(callback, "Shouldn't fail to make the callback!");

    // Null the callback first because OnInputStreamReady may reenter AsyncWait
    mCallback = nullptr;
    mCallbackTarget = nullptr;

    callback->OnInputStreamReady(this);
  }
}

NS_IMETHODIMP AndroidCameraInputStream::AsyncWait(nsIInputStreamCallback *aCallback, uint32_t aFlags, uint32_t aRequestedCount, nsIEventTarget *aTarget)
{
  if (aFlags != 0)
    return NS_ERROR_NOT_IMPLEMENTED;

  if (mCallback || mCallbackTarget)
    return NS_ERROR_UNEXPECTED;

  mCallbackTarget = aTarget;
  mCallback = aCallback;

  // What we are being asked for may be present already
  NotifyListeners();
  return NS_OK;
}


NS_IMETHODIMP AndroidCameraInputStream::CloseWithStatus(nsresult status)
{
  AndroidCameraInputStream::doClose();
  return NS_OK;
}

/**
 * AndroidCaptureProvider implementation
 */

NS_IMPL_THREADSAFE_ISUPPORTS0(AndroidCaptureProvider)

AndroidCaptureProvider* AndroidCaptureProvider::sInstance = NULL;

AndroidCaptureProvider::AndroidCaptureProvider() {
}

AndroidCaptureProvider::~AndroidCaptureProvider() {
  AndroidCaptureProvider::sInstance = NULL;
}

nsresult AndroidCaptureProvider::Init(nsACString& aContentType,
                        nsCaptureParams* aParams,
                        nsIInputStream** aStream) {

  NS_ENSURE_ARG_POINTER(aParams);

  NS_ASSERTION(aParams->frameLimit == 0 || aParams->timeLimit == 0,
    "Cannot set both a frame limit and a time limit!");

  nsRefPtr<AndroidCameraInputStream> stream;

  if (aContentType.EqualsLiteral("video/x-raw-yuv")) {
    stream = new AndroidCameraInputStream();
    if (stream) {
      nsresult rv = stream->Init(aContentType, aParams);
      if (NS_FAILED(rv))
        return rv;
    }
    else
      return NS_ERROR_OUT_OF_MEMORY;
  } else {
    NS_NOTREACHED("Should not have asked Android for this type!");
  }
  return CallQueryInterface(stream, aStream);
}

already_AddRefed<AndroidCaptureProvider> GetAndroidCaptureProvider() {
  if (!AndroidCaptureProvider::sInstance) {
    AndroidCaptureProvider::sInstance = new AndroidCaptureProvider();
  }
  nsRefPtr<AndroidCaptureProvider> ret = AndroidCaptureProvider::sInstance;
  return ret.forget();
}
