/* -*- 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 "CanvasRenderingContextHelper.h"
#include "ImageBitmapRenderingContext.h"
#include "ImageEncoder.h"
#include "mozilla/dom/CanvasRenderingContext2D.h"
#include "mozilla/Telemetry.h"
#include "mozilla/UniquePtr.h"
#include "nsContentUtils.h"
#include "nsDOMJSUtils.h"
#include "nsIScriptContext.h"
#include "nsJSUtils.h"
#include "WebGL1Context.h"
#include "WebGL2Context.h"

namespace mozilla {
namespace dom {

void
CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                     nsIGlobalObject* aGlobal,
                                     BlobCallback& aCallback,
                                     const nsAString& aType,
                                     JS::Handle<JS::Value> aParams,
                                     ErrorResult& aRv)
{
  // Encoder callback when encoding is complete.
  class EncodeCallback : public EncodeCompleteCallback
  {
  public:
    EncodeCallback(nsIGlobalObject* aGlobal, BlobCallback* aCallback)
      : mGlobal(aGlobal)
      , mBlobCallback(aCallback) {}

    // This is called on main thread.
    nsresult ReceiveBlob(already_AddRefed<Blob> aBlob)
    {
      RefPtr<Blob> blob = aBlob;

      RefPtr<Blob> newBlob = Blob::Create(mGlobal, blob->Impl());

      ErrorResult rv;
      mBlobCallback->Call(newBlob, rv);

      mGlobal = nullptr;
      mBlobCallback = nullptr;

      return rv.StealNSResult();
    }

    nsCOMPtr<nsIGlobalObject> mGlobal;
    RefPtr<BlobCallback> mBlobCallback;
  };

  RefPtr<EncodeCompleteCallback> callback =
    new EncodeCallback(aGlobal, &aCallback);

  ToBlob(aCx, aGlobal, callback, aType, aParams, aRv);
}

void
CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                     nsIGlobalObject* aGlobal,
                                     EncodeCompleteCallback* aCallback,
                                     const nsAString& aType,
                                     JS::Handle<JS::Value> aParams,
                                     ErrorResult& aRv)
{
  nsAutoString type;
  nsContentUtils::ASCIIToLower(aType, type);

  nsAutoString params;
  bool usingCustomParseOptions;
  aRv = ParseParams(aCx, type, aParams, params, &usingCustomParseOptions);
  if (aRv.Failed()) {
    return;
  }

  if (mCurrentContext) {
    // We disallow canvases of width or height zero, and set them to 1, so
    // we will have a discrepancy with the sizes of the canvas and the context.
    // That discrepancy is OK, the rest are not.
    nsIntSize elementSize = GetWidthHeight();
    if ((elementSize.width != mCurrentContext->GetWidth() &&
         (elementSize.width != 0 || mCurrentContext->GetWidth() != 1)) ||
        (elementSize.height != mCurrentContext->GetHeight() &&
         (elementSize.height != 0 || mCurrentContext->GetHeight() != 1))) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
  }

  UniquePtr<uint8_t[]> imageBuffer;
  int32_t format = 0;
  if (mCurrentContext) {
    imageBuffer = mCurrentContext->GetImageBuffer(&format);
  }

  RefPtr<EncodeCompleteCallback> callback = aCallback;

  aRv = ImageEncoder::ExtractDataAsync(type,
                                       params,
                                       usingCustomParseOptions,
                                       Move(imageBuffer),
                                       format,
                                       GetWidthHeight(),
                                       callback);
}

already_AddRefed<nsICanvasRenderingContextInternal>
CanvasRenderingContextHelper::CreateContext(CanvasContextType aContextType)
{
  return CreateContextHelper(aContextType, layers::LayersBackend::LAYERS_NONE);
}

already_AddRefed<nsICanvasRenderingContextInternal>
CanvasRenderingContextHelper::CreateContextHelper(CanvasContextType aContextType,
                                                  layers::LayersBackend aCompositorBackend)
{
  MOZ_ASSERT(aContextType != CanvasContextType::NoContext);
  RefPtr<nsICanvasRenderingContextInternal> ret;

  switch (aContextType) {
  case CanvasContextType::NoContext:
    break;

  case CanvasContextType::Canvas2D:
    Telemetry::Accumulate(Telemetry::CANVAS_2D_USED, 1);
    ret = new CanvasRenderingContext2D(aCompositorBackend);
    break;

  case CanvasContextType::WebGL1:
    Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_USED, 1);

    ret = WebGL1Context::Create();
    if (!ret)
      return nullptr;

    break;

  case CanvasContextType::WebGL2:
    Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_USED, 1);

    ret = WebGL2Context::Create();
    if (!ret)
      return nullptr;

    break;

  case CanvasContextType::ImageBitmap:
    ret = new ImageBitmapRenderingContext();

    break;
  }
  MOZ_ASSERT(ret);

  return ret.forget();
}

already_AddRefed<nsISupports>
CanvasRenderingContextHelper::GetContext(JSContext* aCx,
                                         const nsAString& aContextId,
                                         JS::Handle<JS::Value> aContextOptions,
                                         ErrorResult& aRv)
{
  CanvasContextType contextType;
  if (!CanvasUtils::GetCanvasContextType(aContextId, &contextType))
    return nullptr;

  if (!mCurrentContext) {
    // This canvas doesn't have a context yet.
    RefPtr<nsICanvasRenderingContextInternal> context;
    context = CreateContext(contextType);
    if (!context) {
      return nullptr;
    }

    // Ensure that the context participates in CC.  Note that returning a
    // CC participant from QI doesn't addref.
    nsXPCOMCycleCollectionParticipant* cp = nullptr;
    CallQueryInterface(context, &cp);
    if (!cp) {
      aRv.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }

    mCurrentContext = context.forget();
    mCurrentContextType = contextType;

    nsresult rv = UpdateContext(aCx, aContextOptions, aRv);
    if (NS_FAILED(rv)) {
      // See bug 645792 and bug 1215072.
      // We want to throw only if dictionary initialization fails,
      // so only in case aRv has been set to some error value.
      if (contextType == CanvasContextType::WebGL1)
        Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_SUCCESS, 0);
      else if (contextType == CanvasContextType::WebGL2)
        Telemetry::Accumulate(Telemetry::CANVAS_WEBGL2_SUCCESS, 0);
      return nullptr;
    }
    if (contextType == CanvasContextType::WebGL1)
      Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_SUCCESS, 1);
    else if (contextType == CanvasContextType::WebGL2)
      Telemetry::Accumulate(Telemetry::CANVAS_WEBGL2_SUCCESS, 1);
  } else {
    // We already have a context of some type.
    if (contextType != mCurrentContextType)
      return nullptr;
  }

  nsCOMPtr<nsICanvasRenderingContextInternal> context = mCurrentContext;
  return context.forget();
}

nsresult
CanvasRenderingContextHelper::UpdateContext(JSContext* aCx,
                                            JS::Handle<JS::Value> aNewContextOptions,
                                            ErrorResult& aRvForDictionaryInit)
{
  if (!mCurrentContext)
    return NS_OK;

  nsIntSize sz = GetWidthHeight();

  nsCOMPtr<nsICanvasRenderingContextInternal> currentContext = mCurrentContext;

  currentContext->SetIsOpaque(GetOpaqueAttr());

  nsresult rv = currentContext->SetContextOptions(aCx, aNewContextOptions,
                                         aRvForDictionaryInit);
  if (NS_FAILED(rv)) {
    mCurrentContext = nullptr;
    return rv;
  }

  rv = currentContext->SetDimensions(sz.width, sz.height);
  if (NS_FAILED(rv)) {
    mCurrentContext = nullptr;
  }

  return rv;
}

nsresult
CanvasRenderingContextHelper::ParseParams(JSContext* aCx,
                                          const nsAString& aType,
                                          const JS::Value& aEncoderOptions,
                                          nsAString& outParams,
                                          bool* const outUsingCustomParseOptions)
{
  // Quality parameter is only valid for the image/jpeg MIME type
  if (aType.EqualsLiteral("image/jpeg")) {
    if (aEncoderOptions.isNumber()) {
      double quality = aEncoderOptions.toNumber();
      // Quality must be between 0.0 and 1.0, inclusive
      if (quality >= 0.0 && quality <= 1.0) {
        outParams.AppendLiteral("quality=");
        outParams.AppendInt(NS_lround(quality * 100.0));
      }
    }
  }

  // If we haven't parsed the aParams check for proprietary options.
  // The proprietary option -moz-parse-options will take a image lib encoder
  // parse options string as is and pass it to the encoder.
  *outUsingCustomParseOptions = false;
  if (outParams.Length() == 0 && aEncoderOptions.isString()) {
    NS_NAMED_LITERAL_STRING(mozParseOptions, "-moz-parse-options:");
    nsAutoJSString paramString;
    if (!paramString.init(aCx, aEncoderOptions.toString())) {
      return NS_ERROR_FAILURE;
    }
    if (StringBeginsWith(paramString, mozParseOptions)) {
      nsDependentSubstring parseOptions = Substring(paramString,
                                                    mozParseOptions.Length(),
                                                    paramString.Length() -
                                                    mozParseOptions.Length());
      outParams.Append(parseOptions);
      *outUsingCustomParseOptions = true;
    }
  }

  return NS_OK;
}

} // namespace dom
} // namespace mozilla
