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

#include "Image.h"
#include "ImageFactory.h"
#include "imgITools.h"
#include "mozilla/Base64.h"
#include "mozilla/Encoding.h"
#include "mozilla/gtest/MozAssertions.h"
#include "mozilla/Preferences.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/SystemPrincipal.h"
#include "mozilla/UniquePtr.h"
#include "nsComponentManagerUtils.h"
#include "nsIChannel.h"
#include "nsIInputStream.h"
#include "nsILoadInfo.h"
#include "nsISVGPaintContext.h"
#include "nsMimeTypes.h"
#include "nsStreamUtils.h"
#include "nsWindowGfx.h"
#include "SystemPrincipal.h"

#include "gtest/gtest.h"

using namespace mozilla;
using namespace mozilla::image;

const char* SVG_GREEN_CIRCLE =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\" viewBox=\"0 0 100 100\" version=\"1.1\"> \
  <circle fill=\"#00FF00\" stroke=\"#FF0000\" stroke-width=\"20\" cx=\"50\" cy=\"50\" r=\"40\" /> \
</svg> \
";

const char* SVG_UNSIZED_CIRCLE =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \
<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" version=\"1.1\"> \
  <circle fill=\"#00FF00\" stroke=\"#FF0000\" stroke-width=\"20\" cx=\"50\" cy=\"50\" r=\"40\" /> \
</svg> \
";

const char* SVG_CONTEXT_CIRCLE =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\" viewBox=\"0 0 100 100\" version=\"1.1\"> \
  <circle fill=\"context-fill\" stroke=\"context-stroke\" stroke-width=\"20\" cx=\"50\" cy=\"50\" r=\"40\" /> \
</svg> \
";

// Circle's radius is 40 but the total radius includes half the stroke width.
#define CIRCLE_TOTAL_AREA (M_PI * 50 * 50)
// The fill area's radius is the circle's radius minus half the stroke width.
#define CIRCLE_FILL_AREA (M_PI * 30 * 30)
#define CIRCLE_STROKE_AREA (CIRCLE_TOTAL_AREA - CIRCLE_FILL_AREA)

// Allow 2% margin of error to allow for blending
#define ASSERT_NEARLY(val1, val2)   \
  {                                 \
    ASSERT_GT(val1, (val2) * 0.98); \
    ASSERT_LT(val1, (val2) * 1.02); \
  }

class SvgPaintContext : public nsISVGPaintContext {
 public:
  NS_DECL_ISUPPORTS

  nsCString mStrokeColor;
  nsCString mFillColor;

  SvgPaintContext(const char* aStroke, const char* aFill)
      : mStrokeColor(aStroke), mFillColor(aFill) {}

  NS_IMETHODIMP GetStrokeColor(nsACString& color) override {
    color = mStrokeColor;
    return NS_OK;
  }

  NS_IMETHODIMP GetStrokeOpacity(float* opacity) override {
    *opacity = 1.0;
    return NS_OK;
  }

  NS_IMETHODIMP GetFillColor(nsACString& color) override {
    color = mFillColor;
    return NS_OK;
  }

  NS_IMETHODIMP GetFillOpacity(float* opacity) override {
    *opacity = 1.0;
    return NS_OK;
  }

 private:
  virtual ~SvgPaintContext() {};
};

NS_IMPL_ISUPPORTS(SvgPaintContext, nsISVGPaintContext);

class ImageLoadListener : public IProgressObserver {
 public:
  NS_INLINE_DECL_REFCOUNTING(ImageLoadListener, override)

  virtual void OnLoadComplete(bool aLastPart) override { mIsLoaded = true; }

  // Other notifications are ignored.
  virtual void Notify(int32_t aType,
                      const nsIntRect* aRect = nullptr) override {}
  virtual void SetHasImage() override {}
  virtual bool NotificationsDeferred() const override { return false; }
  virtual void MarkPendingNotify() override {}
  virtual void ClearPendingNotify() override {}

  boolean mIsLoaded{};

 private:
  virtual ~ImageLoadListener() {};
};

void LoadImage(const char* aData, imgIContainer** aImage) {
  nsCString svgUri;
  nsresult rv = Base64Encode(aData, strlen(aData), svgUri);
  ASSERT_NS_SUCCEEDED(rv);
  svgUri.Insert("data:" IMAGE_SVG_XML ";base64,", 0);

  nsCOMPtr<nsIURI> uri;
  rv = NS_NewURI(getter_AddRefs(uri), svgUri, UTF_8_ENCODING, nullptr);
  ASSERT_NS_SUCCEEDED(rv);

  nsCOMPtr<nsIPrincipal> principal = SystemPrincipal::Get();
  nsCOMPtr<nsIChannel> channel;
  rv = NS_NewChannel(getter_AddRefs(channel), uri, principal,
                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
                     nsContentPolicyType::TYPE_IMAGE);

  auto listener = MakeRefPtr<ImageLoadListener>();
  auto tracker = MakeRefPtr<ProgressTracker>();
  tracker->AddObserver(listener);
  RefPtr<Image> image = ImageFactory::CreateImage(
      channel, tracker, nsCString(IMAGE_SVG_XML), uri, false, 0);
  ASSERT_FALSE(image->HasError());

  nsCOMPtr<nsIInputStream> stream;
  rv = channel->Open(getter_AddRefs(stream));
  ASSERT_NS_SUCCEEDED(rv);

  uint64_t size;
  rv = stream->Available(&size);
  ASSERT_NS_SUCCEEDED(rv);
  ASSERT_EQ(size, strlen(aData));

  rv = image->OnImageDataAvailable(channel, stream, 0, size);
  ASSERT_NS_SUCCEEDED(rv);

  // Let the Image know we've sent all the data.
  rv = image->OnImageDataComplete(channel, NS_OK, true);
  ASSERT_NS_SUCCEEDED(rv);

  // The final load event from the SVG document is dispatched asynchronously so
  // wait for that to happen.
  MOZ_ALWAYS_TRUE(
      SpinEventLoopUntil("windows:widget:TEST(TestWindowGfx, CreateIcon)"_ns,
                         [&listener]() { return listener->mIsLoaded; }));

  image.forget(aImage);
}

void ConvertToRaster(imgIContainer* vectorImage, imgIContainer** aImage) {
  // First we encode it as a png image.
  nsCOMPtr<imgITools> imgTools =
      do_CreateInstance("@mozilla.org/image/tools;1");

  nsCOMPtr<nsIInputStream> stream;
  nsresult rv = imgTools->EncodeImage(vectorImage, "image/png"_ns, u""_ns,
                                      getter_AddRefs(stream));
  ASSERT_NS_SUCCEEDED(rv);

  uint64_t size;
  rv = stream->Available(&size);

  // And then we load the image again as a raster imgIContainer
  RefPtr<image::Image> image =
      ImageFactory::CreateAnonymousImage("image/png"_ns, size);
  RefPtr<ProgressTracker> tracker = image->GetProgressTracker();
  ASSERT_FALSE(image->HasError());

  rv = image->OnImageDataAvailable(nullptr, stream, 0, size);
  ASSERT_NS_SUCCEEDED(rv);

  // Let the Image know we've sent all the data.
  rv = image->OnImageDataComplete(nullptr, NS_OK, true);
  tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
  ASSERT_NS_SUCCEEDED(rv);

  image.forget(aImage);
}

void CountPixels(ICONINFO& ii, BITMAP& bm, double* redCount, double* greenCount,
                 double* blueCount) {
  BITMAPINFOHEADER bi;
  bi.biSize = sizeof(BITMAPINFOHEADER);
  bi.biWidth = bm.bmWidth;
  bi.biHeight = bm.bmHeight;
  bi.biPlanes = 1;
  bi.biBitCount = 32;
  bi.biCompression = BI_RGB;
  bi.biSizeImage = 0;
  bi.biXPelsPerMeter = 0;
  bi.biYPelsPerMeter = 0;
  bi.biClrUsed = 0;
  bi.biClrImportant = 0;

  auto stride = GDI_WIDTHBYTES(bm.bmWidth * 32);
  auto dataLength = stride * bm.bmHeight;

  UniquePtr<uint8_t[]> bitmapData(new uint8_t[dataLength]);

  *redCount = 0;
  *greenCount = 0;
  *blueCount = 0;

  int lines =
      GetDIBits(::GetDC(nullptr), ii.hbmColor, 0, (UINT)bm.bmHeight,
                (void*)bitmapData.get(), (BITMAPINFO*)&bi, DIB_RGB_COLORS);
  if (lines != bm.bmHeight) {
    return;
  }

  for (long y = 0; y < bm.bmHeight; y++) {
    auto index = stride * y;

    for (long x = 0; x < bm.bmWidth; x++) {
      // Pixels are in BGRA format.
      double blue = bitmapData[index++] / 255.0;
      double green = bitmapData[index++] / 255.0;
      double red = bitmapData[index++] / 255.0;
      double alpha = bitmapData[index++] / 255.0;

      *redCount += red * alpha;
      *greenCount += green * alpha;
      *blueCount += blue * alpha;
    }
  }
}

// Tests that we can scale down an image
TEST(TestWindowGfx, CreateIcon_ScaledDown)
{
  auto Test = [](imgIContainer* image) {
    HICON icon;
    nsresult rv =
        nsWindowGfx::CreateIcon(image, nullptr, false, LayoutDeviceIntPoint(),
                                LayoutDeviceIntSize(50, 50), &icon);
    ASSERT_NS_SUCCEEDED(rv);

    ICONINFO ii;
    BOOL fResult = ::GetIconInfo(icon, &ii);
    ASSERT_TRUE(fResult);

    BITMAP bm;
    fResult = ::GetObject(ii.hbmColor, sizeof(bm), &bm) == sizeof(bm);
    ASSERT_TRUE(fResult);

    ASSERT_EQ(bm.bmWidth, 50);
    ASSERT_EQ(bm.bmHeight, 50);

    double redCount, greenCount, blueCount;
    CountPixels(ii, bm, &redCount, &greenCount, &blueCount);

    // We've scaled the image down to a quarter of its size.
    double fillArea = CIRCLE_FILL_AREA / 4;
    double strokeArea = CIRCLE_STROKE_AREA / 4;

    ASSERT_NEARLY(redCount, strokeArea);
    ASSERT_NEARLY(greenCount, fillArea);
    ASSERT_EQ(blueCount, 0.0);

    if (ii.hbmMask) DeleteObject(ii.hbmMask);
    if (ii.hbmColor) DeleteObject(ii.hbmColor);

    ::DestroyIcon(icon);
  };

  nsCOMPtr<imgIContainer> vectorImage;
  LoadImage(SVG_GREEN_CIRCLE, getter_AddRefs(vectorImage));

  Test(vectorImage);

  nsCOMPtr<imgIContainer> rasterImage;
  ConvertToRaster(vectorImage, getter_AddRefs(rasterImage));

  Test(rasterImage);
}

// Tests that we can scale up an image
TEST(TestWindowGfx, CreateIcon_ScaledUp)
{
  auto Test = [](imgIContainer* image) {
    HICON icon;
    nsresult rv =
        nsWindowGfx::CreateIcon(image, nullptr, false, LayoutDeviceIntPoint(),
                                LayoutDeviceIntSize(200, 200), &icon);
    ASSERT_NS_SUCCEEDED(rv);

    ICONINFO ii;
    BOOL fResult = ::GetIconInfo(icon, &ii);
    ASSERT_TRUE(fResult);

    BITMAP bm;
    fResult = ::GetObject(ii.hbmColor, sizeof(bm), &bm) == sizeof(bm);
    ASSERT_TRUE(fResult);

    ASSERT_EQ(bm.bmWidth, 200);
    ASSERT_EQ(bm.bmHeight, 200);

    double redCount, greenCount, blueCount;
    CountPixels(ii, bm, &redCount, &greenCount, &blueCount);

    // We've scaled the image up to four times its size.
    double fillArea = CIRCLE_FILL_AREA * 4;
    double strokeArea = CIRCLE_STROKE_AREA * 4;

    ASSERT_NEARLY(redCount, strokeArea);
    ASSERT_NEARLY(greenCount, fillArea);
    ASSERT_EQ(blueCount, 0.0);

    if (ii.hbmMask) DeleteObject(ii.hbmMask);
    if (ii.hbmColor) DeleteObject(ii.hbmColor);

    ::DestroyIcon(icon);
  };

  nsCOMPtr<imgIContainer> vectorImage;
  LoadImage(SVG_GREEN_CIRCLE, getter_AddRefs(vectorImage));

  Test(vectorImage);

  nsCOMPtr<imgIContainer> rasterImage;
  ConvertToRaster(vectorImage, getter_AddRefs(rasterImage));

  Test(rasterImage);
}

// Tests that we can render an image at its intrinsic size
TEST(TestWindowGfx, CreateIcon_Intrinsic)
{
  auto Test = [](imgIContainer* image) {
    HICON icon;
    nsresult rv =
        nsWindowGfx::CreateIcon(image, nullptr, false, LayoutDeviceIntPoint(),
                                LayoutDeviceIntSize(), &icon);
    ASSERT_NS_SUCCEEDED(rv);

    ICONINFO ii;
    BOOL fResult = ::GetIconInfo(icon, &ii);
    ASSERT_TRUE(fResult);

    BITMAP bm;
    fResult = ::GetObject(ii.hbmColor, sizeof(bm), &bm) == sizeof(bm);
    ASSERT_TRUE(fResult);

    ASSERT_EQ(bm.bmWidth, 100);
    ASSERT_EQ(bm.bmHeight, 100);

    double redCount, greenCount, blueCount;
    CountPixels(ii, bm, &redCount, &greenCount, &blueCount);

    ASSERT_NEARLY(redCount, CIRCLE_STROKE_AREA);
    ASSERT_NEARLY(greenCount, CIRCLE_FILL_AREA);
    ASSERT_EQ(blueCount, 0.0);

    if (ii.hbmMask) DeleteObject(ii.hbmMask);
    if (ii.hbmColor) DeleteObject(ii.hbmColor);

    ::DestroyIcon(icon);
  };

  nsCOMPtr<imgIContainer> vectorImage;
  LoadImage(SVG_GREEN_CIRCLE, getter_AddRefs(vectorImage));

  Test(vectorImage);

  nsCOMPtr<imgIContainer> rasterImage;
  ConvertToRaster(vectorImage, getter_AddRefs(rasterImage));

  Test(rasterImage);
}

// If an SVG has no intrinsic size and we don't provide one we fail.
TEST(TestWindowGfx, CreateIcon_SVG_NoSize)
{
  nsCOMPtr<imgIContainer> image;
  LoadImage(SVG_UNSIZED_CIRCLE, getter_AddRefs(image));

  HICON icon;
  nsresult rv =
      nsWindowGfx::CreateIcon(image, nullptr, false, LayoutDeviceIntPoint(),
                              LayoutDeviceIntSize(), &icon);
  ASSERT_EQ(rv, NS_ERROR_FAILURE);
}

// But we can still render an SVG with no intrinsic size as long as we provide
// one.
TEST(TestWindowGfx, CreateIcon_SVG_NoIntrinsic)
{
  nsCOMPtr<imgIContainer> image;
  LoadImage(SVG_UNSIZED_CIRCLE, getter_AddRefs(image));

  HICON icon;
  nsresult rv =
      nsWindowGfx::CreateIcon(image, nullptr, false, LayoutDeviceIntPoint(),
                              LayoutDeviceIntSize(200, 200), &icon);
  ASSERT_NS_SUCCEEDED(rv);

  ICONINFO ii;
  BOOL fResult = ::GetIconInfo(icon, &ii);
  ASSERT_TRUE(fResult);

  BITMAP bm;
  fResult = ::GetObject(ii.hbmColor, sizeof(bm), &bm) == sizeof(bm);
  ASSERT_TRUE(fResult);

  ASSERT_EQ(bm.bmWidth, 200);
  ASSERT_EQ(bm.bmHeight, 200);

  double redCount, greenCount, blueCount;
  CountPixels(ii, bm, &redCount, &greenCount, &blueCount);

  // We've scaled the image up to four times its size.
  double fillArea = CIRCLE_FILL_AREA * 4;
  double strokeArea = CIRCLE_STROKE_AREA * 4;

  ASSERT_NEARLY(redCount, strokeArea);
  ASSERT_NEARLY(greenCount, fillArea);
  ASSERT_EQ(blueCount, 0.0);

  if (ii.hbmMask) DeleteObject(ii.hbmMask);
  if (ii.hbmColor) DeleteObject(ii.hbmColor);

  ::DestroyIcon(icon);
}

// Tests that we can set SVG context-fill and context-stroke
TEST(TestWindowGfx, CreateIcon_SVG_Context)
{
  // Normally the context properties don't work for content documents including
  // data URIs.
  Preferences::SetBool("svg.context-properties.content.enabled", true);
  // This test breaks if color management is enabled and an earlier gtest may
  // have enabled it.
  gfxPlatform::SetCMSModeOverride(CMSMode::Off);

  nsCOMPtr<imgIContainer> image;
  LoadImage(SVG_CONTEXT_CIRCLE, getter_AddRefs(image));

  nsCOMPtr<nsISVGPaintContext> paintContext =
      new SvgPaintContext("#00FF00", "#0000FF");

  HICON icon;
  nsresult rv = nsWindowGfx::CreateIcon(image, paintContext, false,
                                        LayoutDeviceIntPoint(),
                                        LayoutDeviceIntSize(200, 200), &icon);
  ASSERT_NS_SUCCEEDED(rv);

  ICONINFO ii;
  BOOL fResult = ::GetIconInfo(icon, &ii);
  ASSERT_TRUE(fResult);

  BITMAP bm;
  fResult = ::GetObject(ii.hbmColor, sizeof(bm), &bm) == sizeof(bm);
  ASSERT_TRUE(fResult);

  ASSERT_EQ(bm.bmWidth, 200);
  ASSERT_EQ(bm.bmHeight, 200);

  double redCount, greenCount, blueCount;
  CountPixels(ii, bm, &redCount, &greenCount, &blueCount);

  // We've scaled the image up to four times its size.
  double fillArea = CIRCLE_FILL_AREA * 4;
  double strokeArea = CIRCLE_STROKE_AREA * 4;

  ASSERT_NEARLY(greenCount, strokeArea);
  ASSERT_NEARLY(blueCount, fillArea);
  ASSERT_EQ(redCount, 0.0);

  if (ii.hbmMask) DeleteObject(ii.hbmMask);
  if (ii.hbmColor) DeleteObject(ii.hbmColor);

  ::DestroyIcon(icon);
}
