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

#include "imgINotificationObserver.h"
#include "imgITools.h"
#include "imgINotificationObserver.h"
#include "gfxUtils.h"
#include "AndroidGraphics.h"
#include "JavaExceptions.h"
#include "mozilla/gfx/Point.h"
#include "mozilla/gfx/Swizzle.h"
#include "mozilla/java/ImageWrappers.h"
#include "nsNetUtil.h"

using namespace mozilla::gfx;

namespace mozilla {
namespace widget {

namespace {

class ImageCallbackHelper;

HashSet<RefPtr<ImageCallbackHelper>, PointerHasher<ImageCallbackHelper*>>
    gDecodeRequests;

class ImageCallbackHelper : public imgIContainerCallback,
                            public imgINotificationObserver {
 public:
  NS_DECL_ISUPPORTS

  void CompleteExceptionally(nsresult aRv) {
    nsPrintfCString error("Could not process image: 0x%08X", aRv);
    mResult->CompleteExceptionally(
        java::Image::ImageProcessingException::New(error.get())
            .Cast<jni::Throwable>());
    gDecodeRequests.remove(this);
  }

  void Complete(DataSourceSurface::ScopedMap& aSourceSurface, int32_t width,
                int32_t height) {
    auto pixels = mozilla::jni::ByteBuffer::New(
        reinterpret_cast<int8_t*>(aSourceSurface.GetData()),
        aSourceSurface.GetStride() * height);
    auto bitmap = java::sdk::Bitmap::CreateBitmap(
        width, height, java::sdk::Bitmap::Config::ARGB_8888());
    bitmap->CopyPixelsFromBuffer(pixels);
    mResult->Complete(bitmap);
    gDecodeRequests.remove(this);
  }

  ImageCallbackHelper(java::GeckoResult::Param aResult, int32_t aDesiredLength)
      : mResult(aResult), mDesiredLength(aDesiredLength), mImage(nullptr) {
    MOZ_ASSERT(mResult);
  }

  NS_IMETHOD
  OnImageReady(imgIContainer* aImage, nsresult aStatus) override {
    // Let's make sure we are alive until the request completes
    MOZ_ALWAYS_TRUE(gDecodeRequests.putNew(this));

    if (NS_FAILED(aStatus)) {
      CompleteExceptionally(aStatus);
      return aStatus;
    }

    mImage = aImage;
    return mImage->StartDecoding(
        imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY,
        imgIContainer::FRAME_FIRST);
  }

  // This method assumes that the image is ready to be processed
  nsresult SendBitmap() {
    RefPtr<gfx::SourceSurface> surface;

    NS_ENSURE_TRUE(mImage, NS_ERROR_FAILURE);
    if (mDesiredLength > 0) {
      surface = mImage->GetFrameAtSize(
          gfx::IntSize(mDesiredLength, mDesiredLength),
          imgIContainer::FRAME_FIRST,
          imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
    } else {
      surface = mImage->GetFrame(
          imgIContainer::FRAME_FIRST,
          imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
    }

    NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
    RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();

    NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
    int32_t width = dataSurface->GetSize().width;
    int32_t height = dataSurface->GetSize().height;

    DataSourceSurface::ScopedMap sourceMap(dataSurface,
                                           DataSourceSurface::READ);

    // Android's Bitmap only supports R8G8B8A8, so we need to convert the
    // data to the right format
    RefPtr<DataSourceSurface> destDataSurface =
        Factory::CreateDataSourceSurfaceWithStride(dataSurface->GetSize(),
                                                   SurfaceFormat::R8G8B8A8,
                                                   sourceMap.GetStride());
    NS_ENSURE_TRUE(destDataSurface, NS_ERROR_FAILURE);

    DataSourceSurface::ScopedMap destMap(destDataSurface,
                                         DataSourceSurface::READ_WRITE);

    SwizzleData(sourceMap.GetData(), sourceMap.GetStride(),
                surface->GetFormat(), destMap.GetData(), destMap.GetStride(),
                SurfaceFormat::R8G8B8A8, destDataSurface->GetSize());

    Complete(destMap, width, height);

    return NS_OK;
  }

  void Notify(imgIRequest* aRequest, int32_t aType,
              const nsIntRect* aData) override {
    if (aType == imgINotificationObserver::DECODE_COMPLETE) {
      nsresult status = SendBitmap();
      if (NS_FAILED(status)) {
        CompleteExceptionally(status);
      }

      // Breack the cyclic reference between `ImageDecoderListener` (which is a
      // `imgIContainer`) and `ImageCallbackHelper`.
      mImage = nullptr;
    }
  }

 private:
  const java::GeckoResult::GlobalRef mResult;
  int32_t mDesiredLength;
  nsCOMPtr<imgIContainer> mImage;
  virtual ~ImageCallbackHelper() {}
};

NS_IMPL_ISUPPORTS(ImageCallbackHelper, imgIContainerCallback,
                  imgINotificationObserver)

}  // namespace

/* static */ void ImageDecoderSupport::Decode(jni::String::Param aUri,
                                              int32_t aDesiredLength,
                                              jni::Object::Param aResult) {
  auto result = java::GeckoResult::LocalRef(aResult);
  RefPtr<ImageCallbackHelper> helper =
      new ImageCallbackHelper(result, aDesiredLength);

  nsresult rv = DecodeInternal(aUri->ToString(), helper, helper);
  if (NS_FAILED(rv)) {
    helper->OnImageReady(nullptr, rv);
  }
}

/* static */ nsresult ImageDecoderSupport::DecodeInternal(
    const nsAString& aUri, imgIContainerCallback* aCallback,
    imgINotificationObserver* aObserver) {
  nsCOMPtr<imgITools> imgTools = do_GetService("@mozilla.org/image/tools;1");
  if (NS_WARN_IF(!imgTools)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), aUri);
  NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);

  nsCOMPtr<nsIChannel> channel;
  rv = NS_NewChannel(getter_AddRefs(channel), uri,
                     nsContentUtils::GetSystemPrincipal(),
                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
                     nsIContentPolicy::TYPE_IMAGE);
  NS_ENSURE_SUCCESS(rv, rv);

  return imgTools->DecodeImageFromChannelAsync(uri, channel, aCallback,
                                               aObserver);
}

}  // namespace widget
}  // namespace mozilla