/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/image/image_resizer.h"
#include "pagespeed/kernel/image/jpeg_optimizer.h"
#include "pagespeed/kernel/image/png_optimizer.h"
#include "pagespeed/kernel/image/read_image.h"
#include "pagespeed/kernel/image/test_utils.h"
#include "pagespeed/kernel/image/webp_optimizer.h"

namespace {

// Pixel formats
using net_instaweb::MessageHandler;
using net_instaweb::MockMessageHandler;
using net_instaweb::NullMutex;
using pagespeed::image_compression::PixelFormat;
using pagespeed::image_compression::GRAY_8;
using pagespeed::image_compression::RGB_888;
using pagespeed::image_compression::RGBA_8888;
// Readers and writers
using pagespeed::image_compression::JpegCompressionOptions;
using pagespeed::image_compression::JpegScanlineWriter;
using pagespeed::image_compression::kPngSuiteTestDir;
using pagespeed::image_compression::kPngTestDir;
using pagespeed::image_compression::kResizedTestDir;
using pagespeed::image_compression::PngScanlineReaderRaw;
using pagespeed::image_compression::ReadTestFile;
using pagespeed::image_compression::ScanlineResizer;
using pagespeed::image_compression::ScanlineWriterInterface;
using pagespeed::image_compression::WebpConfiguration;
using pagespeed::image_compression::kMessagePatternPixelFormat;
using pagespeed::image_compression::kMessagePatternStats;
using pagespeed::image_compression::kMessagePatternUnexpectedEOF;
using pagespeed::image_compression::kMessagePatternWritingToWebp;

const size_t kPreserveAspectRatio =
    pagespeed::image_compression::ScanlineResizer::kPreserveAspectRatio;

// Three testing images: GRAY_8, RGB_888, and RGBA_8888.
// Size of these images is 32-by-32 pixels.
const char* kValidImages[] = {
    "basi0g04",
    "basi3p02",
    "basn6a16",
};

// Image of RGBA_8888 format. Size is 128-by-128 pixels.
const char kImagePagespeed[] = "pagespeed-128";
// Same content as pagespeed-128, but resized to 33-by-34 pixels.
const char kImagePageSpeed33x34[] = "pagespeed-33x34";
// Image with 4096-by-2048 pixels.
const char kLarge4096x2048[] = "large";

// Size of the output image [width, height]. The size of the input image
// is 32-by-32. We would like to test resizing ratios of both integers
// and non-integers.
const size_t kOutputSize[][2] = {
    {16, kPreserveAspectRatio},   // Shrink image by 2 times in both directions.
    {kPreserveAspectRatio, 8},    // Shrink image by 4 times in both directions.
    {3, 3},    // Shrink image by 32/3 times in both directions.
    {16, 25},  // Shrink image by [2, 32/25] times.
    {32, 5},   // Shrink image by [1, 32/5] times.
    {3, 32},   // Shrink image by [32/3, 1] times.
    {31, 31},  // Shrink image by [32/31, 32/31] times.
    {32, 32},  // Although the image did not shrink, the algorithm is exercised.
};

const size_t kValidImageCount = arraysize(kValidImages);
const size_t KOutputSizeCount = arraysize(kOutputSize);

class ScanlineResizerTest : public testing::Test {
 public:
  ScanlineResizerTest() :
      message_handler_(new NullMutex),
      reader_(&message_handler_),
      resizer_(&message_handler_),
      scanline_(NULL) {
  }

 protected:
  void InitializeReader(const char* file_name) {
    ASSERT_TRUE(ReadTestFile(kPngSuiteTestDir, file_name, "png",
                             &input_image_));
    ASSERT_TRUE(reader_.Initialize(input_image_.data(), input_image_.length()));
  }

  void ResizeAndValidateImage(const char* file_name, const GoogleString& image);


  MockMessageHandler message_handler_;
  PngScanlineReaderRaw reader_;
  ScanlineResizer resizer_;
  GoogleString input_image_;
  void* scanline_;

 private:
  DISALLOW_COPY_AND_ASSIGN(ScanlineResizerTest);
};

// Read the gold file. Size of the gold file is embedded in its name.
// For example 'testdata/resized/basi0g04_w16_h16.png'.
bool ReadGoldImageToString(const char* file_name,
                           size_t width,
                           size_t height,
                           GoogleString* image_data) {
  GoogleString gold_file_name = StringPrintf("%s_w%d_h%d", file_name,
      static_cast<int>(width), static_cast<int>(height));
  return ReadTestFile(kResizedTestDir, gold_file_name.c_str(), "png",
                      image_data);
}

// Return JPEG writer for Gray_8, or WebP writer for RGB_888 or RGBA_8888.
ScanlineWriterInterface* CreateWriter(PixelFormat pixel_format,
                                      size_t width,
                                      size_t height,
                                      GoogleString* image_data,
                                      GoogleString* file_ext,
                                      MessageHandler* handler) {
  if (pixel_format == GRAY_8) {
    *file_ext = "jpg";
    JpegCompressionOptions jpeg_config;
    jpeg_config.lossy = true;
    jpeg_config.lossy_options.quality = 100;
    return reinterpret_cast<ScanlineWriterInterface*>(
        CreateScanlineWriter(pagespeed::image_compression::IMAGE_JPEG,
                             pixel_format, width, height, &jpeg_config,
                             image_data, handler));
  } else {
    *file_ext = "webp";
    WebpConfiguration webp_config;  // Use lossless by default
    return reinterpret_cast<ScanlineWriterInterface*>(
        CreateScanlineWriter(pagespeed::image_compression::IMAGE_WEBP,
                             pixel_format, width, height, &webp_config,
                             image_data, handler));
  }
}

// Make sure the resized results, include image size, pixel format, and pixel
// values, match the gold data. The gold data has the image size coded in the
// file name. For example, an image resized to 16-by-16 is
// third_party/pagespeed/kernel/image/testdata/resized/basi0g04_w16_h16.png
void ScanlineResizerTest::ResizeAndValidateImage(const char* file_name,
                                                 const GoogleString& image) {
  PngScanlineReaderRaw gold_reader(&message_handler_);
  for (size_t index_size = 0; index_size < KOutputSizeCount; ++index_size) {
    size_t width = kOutputSize[index_size][0];
    size_t height = kOutputSize[index_size][1];
    ASSERT_TRUE(reader_.Initialize(image.data(), image.length()));
    ASSERT_TRUE(resizer_.Initialize(&reader_, width, height));

    if (width == 0) width = height;
    if (height == 0) height = width;
    GoogleString gold_image;
    ASSERT_TRUE(ReadGoldImageToString(file_name, width, height, &gold_image));
    ASSERT_TRUE(gold_reader.Initialize(gold_image.data(), gold_image.length()));

    // Make sure the images sizes and the pixel formats are the same.
    ASSERT_EQ(gold_reader.GetImageWidth(), resizer_.GetImageWidth());
    ASSERT_EQ(gold_reader.GetImageHeight(), resizer_.GetImageHeight());
    ASSERT_EQ(gold_reader.GetPixelFormat(), resizer_.GetPixelFormat());

    while (resizer_.HasMoreScanLines() && gold_reader.HasMoreScanLines()) {
      uint8* resized_scanline = NULL;
      uint8* gold_scanline = NULL;
      ASSERT_TRUE(resizer_.ReadNextScanline(
          reinterpret_cast<void**>(&resized_scanline)));
      ASSERT_TRUE(gold_reader.ReadNextScanline(
          reinterpret_cast<void**>(&gold_scanline)));

      for (size_t i = 0; i < resizer_.GetBytesPerScanline(); ++i) {
        ASSERT_EQ(gold_scanline[i], resized_scanline[i]);
      }
    }

    // Make sure both the resizer and the reader have exhausted scanlines.
    ASSERT_FALSE(resizer_.HasMoreScanLines());
    ASSERT_FALSE(gold_reader.HasMoreScanLines());
  }
}

TEST_F(ScanlineResizerTest, Accuracy) {
  // Test accuracy of resizing for some images in PNG Suite. All images in this
  // suite have 32-by-32 pixels.
  for (size_t index_image = 0; index_image < kValidImageCount; ++index_image) {
    const char* file_name = kValidImages[index_image];
    ASSERT_TRUE(ReadTestFile(kPngSuiteTestDir, file_name, "png",
                             &input_image_));
    ResizeAndValidateImage(file_name, input_image_);
  }

  // Test accuracy of resizing for an image with 33-by-34 pixels.
  ASSERT_TRUE(ReadTestFile(kPngTestDir, kImagePageSpeed33x34, "png",
                           &input_image_));
  ResizeAndValidateImage(kImagePageSpeed33x34, input_image_);
}

// Resize the image and write the result to a JPEG or a WebP image.
TEST_F(ScanlineResizerTest, ResizeAndWrite) {
  message_handler_.AddPatternToSkipPrinting(kMessagePatternPixelFormat);
  message_handler_.AddPatternToSkipPrinting(kMessagePatternStats);
  message_handler_.AddPatternToSkipPrinting(kMessagePatternWritingToWebp);
  for (size_t index_image = 0; index_image < kValidImageCount; ++index_image) {
    const char* file_name = kValidImages[index_image];
    ASSERT_TRUE(ReadTestFile(kPngSuiteTestDir, file_name, "png",
                             &input_image_));

    for (size_t index_size = 0; index_size < KOutputSizeCount; ++index_size) {
      const size_t width = kOutputSize[index_size][0];
      const size_t height = kOutputSize[index_size][1];
      ASSERT_TRUE(reader_.Initialize(input_image_.data(),
                                     input_image_.length()));
      ASSERT_TRUE(resizer_.Initialize(&reader_, width, height));

      GoogleString output_image;
      GoogleString file_ext;
      net_instaweb::scoped_ptr<ScanlineWriterInterface> writer(
         CreateWriter(resizer_.GetPixelFormat(),
                      resizer_.GetImageWidth(),
                      resizer_.GetImageHeight(),
                      &output_image,
                      &file_ext,
                      &message_handler_));

      while (resizer_.HasMoreScanLines()) {
        ASSERT_TRUE(resizer_.ReadNextScanline(&scanline_));
        ASSERT_TRUE(writer->WriteNextScanline(scanline_));
      }

      ASSERT_TRUE(writer->FinalizeWrite());
    }
  }
}

// Both width and height are specified.
TEST_F(ScanlineResizerTest, InitializeWidthHeight) {
  size_t width = 20;
  size_t height = 10;
  InitializeReader(kValidImages[0]);
  ASSERT_TRUE(resizer_.Initialize(&reader_, width, height));
  EXPECT_EQ(width, resizer_.GetImageWidth());
  EXPECT_EQ(height, resizer_.GetImageHeight());
  EXPECT_EQ(reader_.GetPixelFormat(), resizer_.GetPixelFormat());
}

// Only height is specified.
TEST_F(ScanlineResizerTest, InitializeHeight) {
  size_t width = kPreserveAspectRatio;
  size_t height = 10;
  InitializeReader(kValidImages[0]);
  ASSERT_TRUE(resizer_.Initialize(&reader_, width, height));
  EXPECT_EQ(height, resizer_.GetImageWidth());
  EXPECT_EQ(height, resizer_.GetImageHeight());
}

// Only width is specified.
TEST_F(ScanlineResizerTest, InitializeWidth) {
  size_t width = 12;
  size_t height = kPreserveAspectRatio;
  InitializeReader(kValidImages[0]);
  ASSERT_TRUE(resizer_.Initialize(&reader_, width, height));
  EXPECT_EQ(width, resizer_.GetImageWidth());
  EXPECT_EQ(width, resizer_.GetImageHeight());
}

// The resizer is not initialized, so ReadNextScanline returns false.
TEST_F(ScanlineResizerTest, ReadNullScanline) {
#ifndef NDEBUG
  ASSERT_DEATH(resizer_.ReadNextScanline(&scanline_),
               "SCANLINE_RESIZER/SCANLINE_STATUS_INVOCATION_ERROR "
               "null reader or no more scanlines");
#else
  ASSERT_FALSE(resizer_.ReadNextScanline(&scanline_));
#endif
}

// The resizer has only one scanline, so ReadNextScanline returns false
// at the second call.
TEST_F(ScanlineResizerTest, ReadNextScanline) {
  InitializeReader(kValidImages[1]);
  ASSERT_TRUE(resizer_.Initialize(&reader_, 10, 1));
  ASSERT_TRUE(resizer_.ReadNextScanline(&scanline_));
#ifndef NDEBUG
  ASSERT_DEATH(resizer_.ReadNextScanline(&scanline_),
               "SCANLINE_RESIZER/SCANLINE_STATUS_INVOCATION_ERROR "
               "null reader or no more scanlines");
#else
  ASSERT_FALSE(resizer_.ReadNextScanline(&scanline_));
#endif
}

// The original image is truncated. Only 100 bytes are passed to the reader.
// The reader is able decode the image header, but not the pixels.
// The resizer should return false when the reader failes.
TEST_F(ScanlineResizerTest, BadReader) {
  message_handler_.AddPatternToSkipPrinting(kMessagePatternUnexpectedEOF);
  ASSERT_TRUE(ReadTestFile(kPngSuiteTestDir, kValidImages[0], "png",
                           &input_image_));
  ASSERT_TRUE(reader_.Initialize(input_image_.data(), 100));
  ASSERT_TRUE(resizer_.Initialize(&reader_, 10, 20));
  ASSERT_FALSE(resizer_.ReadNextScanline(&scanline_));
}

// The resizer is initialized twice and only a portion of scanlines are readed.
// The resizer should not have any error.
TEST_F(ScanlineResizerTest, PartialRead) {
  InitializeReader(kValidImages[0]);

  ASSERT_TRUE(resizer_.Initialize(&reader_, 10, 20));
  // Read only 1 scanline, although there are 20.
  EXPECT_TRUE(resizer_.ReadNextScanline(&scanline_));

  ASSERT_TRUE(resizer_.Initialize(&reader_, 10, 20));
  // Read only 2 scanlines, although there are 20.
  EXPECT_TRUE(resizer_.ReadNextScanline(&scanline_));
  EXPECT_TRUE(resizer_.ReadNextScanline(&scanline_));
}

// Resize the image by non-integer ratios.
TEST_F(ScanlineResizerTest, ResizeFractionalRatio) {
  const int new_width = 11;
  const int new_height = 19;
  ASSERT_TRUE(ReadTestFile(kPngTestDir, kImagePagespeed, "png", &input_image_));

  ASSERT_TRUE(reader_.Initialize(input_image_.data(), input_image_.length()));
  ASSERT_TRUE(resizer_.Initialize(&reader_, new_width, new_height));

  int num_rows = 0;
  while (resizer_.HasMoreScanLines()) {
    ASSERT_TRUE(resizer_.ReadNextScanline(&scanline_));
    ++num_rows;
  }
  EXPECT_EQ(new_height, num_rows);
}

TEST_F(ScanlineResizerTest, LargeImage) {
  ASSERT_TRUE(ReadTestFile(kPngTestDir, kLarge4096x2048, "png", &input_image_));
  ResizeAndValidateImage(kLarge4096x2048, input_image_);
}

}  // namespace
