/*
 * 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 <vector>

#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/string.h"
#include "pagespeed/kernel/image/jpeg_optimizer.h"
#include "pagespeed/kernel/image/jpeg_optimizer_test_helper.h"
#include "pagespeed/kernel/image/test_utils.h"

// DO NOT INCLUDE LIBJPEG HEADERS HERE. Doing so causes build errors
// on Windows. If you need to call out to libjpeg, please add helper
// methods in jpeg_optimizer_test_helper.h.

namespace {

using net_instaweb::MockMessageHandler;
using net_instaweb::NullMutex;
using pagespeed::image_compression::ColorSampling;
using pagespeed::image_compression::JpegCompressionOptions;
using pagespeed::image_compression::JpegLossyOptions;
using pagespeed::image_compression::kJpegTestDir;
using pagespeed::image_compression::OptimizeJpeg;
using pagespeed::image_compression::OptimizeJpegWithOptions;
using pagespeed::image_compression::ReadTestFileWithExt;
using pagespeed_testing::image_compression::GetNumScansInJpeg;
using pagespeed_testing::image_compression::GetJpegNumComponentsAndSamplingFactors;  // NOLINT
using pagespeed_testing::image_compression::IsJpegSegmentPresent;
using pagespeed_testing::image_compression::GetColorProfileMarker;
using pagespeed_testing::image_compression::GetExifDataMarker;

// The JPEG_TEST_DIR_PATH macro is set by the gyp target that builds this file.
const char kAppSegmentsJpegFile[] = "app_segments.jpg";

struct ImageCompressionInfo {
  const char* filename;
  size_t original_size;
  size_t compressed_size;
  size_t lossy_compressed_size;
  size_t progressive_size;
  size_t progressive_and_lossy_compressed_size;
};

ImageCompressionInfo kValidImages[] = {
  { "sjpeg1.jpg", 1552, 1536, 1165, 1774, 1410 },
  { "sjpeg3.jpg", 44084, 41664, 26924, 40997, 25814 },
  { "sjpeg6.jpg", 149600, 147163, 89671, 146038, 84641 },
  { "testgray.jpg", 5014, 3072, 3060, 3094, 3078 },
  { "sjpeg2.jpg", 3612, 3283, 3652, 3475, 3833 },
  { "sjpeg4.jpg", 168895, 168240, 50936, 162867, 48731 },
  { "test411.jpg", 6883, 4367, 3705, 4540, 3849 },
  { "test420.jpg", 6173, 3657, 3651, 3796, 3787 },
  { "test422.jpg", 6501, 3985, 3709, 4152, 3852 },
};

const char *kInvalidFiles[] = {
  "notajpeg.png",   // A png.
  "notajpeg.gif",   // A gif.
  "emptyfile.jpg",  // A zero-byte file.
  "corrupt.jpg",    // Invalid huffman code in the image data section.
};

const size_t kValidImageCount = arraysize(kValidImages);
const size_t kInvalidFileCount = arraysize(kInvalidFiles);

class JpegOptimizerTest : public testing::Test {
 public:
  JpegOptimizerTest()
    : message_handler_(new NullMutex) {
  }

  void AssertColorSampling(const GoogleString& data,
                           int expected_h_sampling_factor,
                           int expected_v_sampling_factor) {
    int num_components, h_sampling_factor, v_sampling_factor;
    ASSERT_TRUE(GetJpegNumComponentsAndSamplingFactors(data,
                                                       &num_components,
                                                       &h_sampling_factor,
                                                       &v_sampling_factor));
    ASSERT_LE(1, num_components);
    ASSERT_EQ(expected_h_sampling_factor, h_sampling_factor);
    ASSERT_EQ(expected_v_sampling_factor, v_sampling_factor);
  }

  void AssertJpegOptimizeWithSampling(
      const GoogleString &src_data, GoogleString* dest_data,
      ColorSampling color_sampling, int h_sampling_factor,
      int v_sampling_factor) {
    dest_data->clear();
    JpegCompressionOptions options;
    options.lossy = true;
    options.lossy_options.color_sampling = color_sampling;

    ASSERT_TRUE(OptimizeJpegWithOptions(src_data, dest_data, options,
                                        &message_handler_));
    AssertColorSampling(*dest_data, h_sampling_factor, v_sampling_factor);
  }

 protected:
  net_instaweb::MockMessageHandler message_handler_;

 private:
  DISALLOW_COPY_AND_ASSIGN(JpegOptimizerTest);
};

TEST_F(JpegOptimizerTest, ValidJpegs) {
  for (size_t i = 0; i < kValidImageCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
    GoogleString dest_data;
    ASSERT_TRUE(OptimizeJpeg(src_data, &dest_data, &message_handler_));
    EXPECT_EQ(kValidImages[i].original_size, src_data.size())
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].compressed_size, dest_data.size())
        << kValidImages[i].filename;

    ASSERT_LE(dest_data.size(), src_data.size());
  }
}

TEST_F(JpegOptimizerTest, ValidJpegsLossy) {
  for (size_t i = 0; i < kValidImageCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
    pagespeed::image_compression::JpegCompressionOptions options;
    options.lossy = true;
    GoogleString dest_data;
    ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                        &message_handler_))
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].original_size, src_data.size())
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].lossy_compressed_size, dest_data.size())
        << kValidImages[i].filename;
  }
}

TEST_F(JpegOptimizerTest, ValidJpegLossyAndColorSampling) {
  int test_422_file_idx = 8;
  GoogleString src_data;
  GoogleString src_filename = kValidImages[test_422_file_idx].filename;
  ReadTestFileWithExt(kJpegTestDir, src_filename.c_str(), &src_data);

  JpegCompressionOptions options;
  options.lossy = true;

  GoogleString dest_data;
  // Calling optimize will use default color sampling which is 420.
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  size_t lossy_420_size =
      kValidImages[test_422_file_idx].lossy_compressed_size;
  EXPECT_EQ(lossy_420_size, dest_data.size()) << src_filename;
  AssertColorSampling(dest_data, 2, 2);

  // Calling optimize with ColorSampling::YUV420 will give samping as 420.
  AssertJpegOptimizeWithSampling(src_data, &dest_data,
                                 pagespeed::image_compression::YUV420, 2, 2);
  EXPECT_EQ(lossy_420_size, dest_data.size()) << src_filename;

  // Calling optimize with ColorSampling::RETAIN will leave samping as 422.
  AssertJpegOptimizeWithSampling(src_data, &dest_data,
                                 pagespeed::image_compression::RETAIN, 2, 1);
  size_t lossy_retain_size = dest_data.size();
  EXPECT_GT(lossy_retain_size, lossy_420_size) << src_filename;

  // Calling optimize with ColorSampling::YUV422 will give samping as 422.
  AssertJpegOptimizeWithSampling(src_data, &dest_data,
                                 pagespeed::image_compression::YUV422, 2, 1);
  EXPECT_EQ(lossy_retain_size, dest_data.size()) << src_filename;

  // Calling optimize with ColorSampling::YUV444 will give samping as 444.
  AssertJpegOptimizeWithSampling(src_data, &dest_data,
                                 pagespeed::image_compression::YUV444, 1, 1);
  EXPECT_LT(lossy_retain_size, dest_data.size()) << src_filename;
}

TEST_F(JpegOptimizerTest, ValidJpegRetainColorProfile) {
  GoogleString src_data;
  ReadTestFileWithExt(kJpegTestDir, kAppSegmentsJpegFile, &src_data);

  GoogleString dest_data;

  // Testing lossless flow.
  JpegCompressionOptions options;
  options.retain_color_profile = true;

  ASSERT_TRUE(IsJpegSegmentPresent(src_data, GetColorProfileMarker()));
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));

  options.retain_color_profile = false;
  dest_data.clear();
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));

  // Testing lossy flow.
  options.lossy = true;
  options.retain_color_profile = true;
  dest_data.clear();
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));

  options.retain_color_profile = false;
  dest_data.clear();
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));
}

TEST_F(JpegOptimizerTest, ValidJpegRetainExifData) {
  GoogleString src_data;
  ReadTestFileWithExt(kJpegTestDir, kAppSegmentsJpegFile, &src_data);

  GoogleString dest_data;

  // Testing lossless flow.
  JpegCompressionOptions options;
  options.retain_exif_data = true;

  ASSERT_TRUE(IsJpegSegmentPresent(src_data, GetExifDataMarker()));
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));

  options.retain_exif_data = false;
  dest_data.clear();
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));

  // Testing lossy flow.
  options.lossy = true;
  options.retain_exif_data = true;
  dest_data.clear();
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));

  options.retain_exif_data = false;
  dest_data.clear();
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));
}

TEST_F(JpegOptimizerTest, ValidJpegLossyWithNProgressiveScans) {
  GoogleString src_data;
  ReadTestFileWithExt(kJpegTestDir, kAppSegmentsJpegFile, &src_data);

  GoogleString dest_data;

  // Testing lossless flow.
  JpegCompressionOptions options;
  options.progressive = true;

  EXPECT_EQ(1, GetNumScansInJpeg(src_data));
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  int num_scans = GetNumScansInJpeg(dest_data);
  EXPECT_LT(1, num_scans);

  dest_data.clear();
  options.lossy = true;
  options.lossy_options.num_scans = 3;
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  EXPECT_EQ(3, GetNumScansInJpeg(dest_data));

  dest_data.clear();
  // jpeg has max scan limit based on image color spaces, So trying to set
  // num scans to a large value should be handled gracefully and default to
  // jpeg scan limit if the specified value is greater.
  options.lossy_options.num_scans = 1000;
  ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                      &message_handler_));
  EXPECT_EQ(num_scans, GetNumScansInJpeg(dest_data));
}

TEST_F(JpegOptimizerTest, ValidJpegsProgressive) {
  for (size_t i = 0; i < kValidImageCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
    pagespeed::image_compression::JpegCompressionOptions options;
    options.progressive = true;
    GoogleString dest_data;
    ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                        &message_handler_))
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].original_size, src_data.size())
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].progressive_size, dest_data.size())
        << kValidImages[i].filename;
  }
}

TEST_F(JpegOptimizerTest, ValidJpegsProgressiveAndLossy) {
  for (size_t i = 0; i < kValidImageCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
    pagespeed::image_compression::JpegCompressionOptions options;
    options.lossy = true;
    options.progressive = true;
    GoogleString dest_data;
    ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                        &message_handler_))
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].original_size, src_data.size())
        << kValidImages[i].filename;
    EXPECT_EQ(kValidImages[i].progressive_and_lossy_compressed_size,
              dest_data.size()) << kValidImages[i].filename;
  }
}

TEST_F(JpegOptimizerTest, InvalidJpegs) {
  for (size_t i = 0; i < kInvalidFileCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
    GoogleString dest_data;
    ASSERT_FALSE(OptimizeJpeg(src_data, &dest_data, &message_handler_));
  }
}

TEST_F(JpegOptimizerTest, InvalidJpegsLossy) {
  for (size_t i = 0; i < kInvalidFileCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
    JpegCompressionOptions options;
    options.lossy = true;
    GoogleString dest_data;
    ASSERT_FALSE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                         &message_handler_));
  }
}

TEST_F(JpegOptimizerTest, InvalidJpegsProgressive) {
  for (size_t i = 0; i < kInvalidFileCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
    JpegCompressionOptions options;
    options.progressive = true;
    GoogleString dest_data;
    ASSERT_FALSE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                         &message_handler_));
  }
}

TEST_F(JpegOptimizerTest, InvalidJpegsProgressiveAndLossy) {
  for (size_t i = 0; i < kInvalidFileCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
    JpegCompressionOptions options;
    options.lossy = true;
    options.progressive = true;
    GoogleString dest_data;
    ASSERT_FALSE(OptimizeJpegWithOptions(src_data, &dest_data, options,
                                         &message_handler_));
  }
}

// Test that after reading an invalid jpeg, the reader cleans its state so that
// it can read a correct jpeg again.
TEST_F(JpegOptimizerTest, CleanupAfterReadingInvalidJpeg) {
  // Compress each input image with a reinitialized JpegOptimizer.
  // We will compare these files with the output we get from
  // a JpegOptimizer that had an error.
  std::vector<GoogleString> correctly_compressed;
  for (size_t i = 0; i < kValidImageCount; ++i) {
    GoogleString src_data;
    ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
    correctly_compressed.push_back("");
    GoogleString &dest_data = correctly_compressed.back();
    ASSERT_TRUE(OptimizeJpeg(src_data, &dest_data, &message_handler_));
  }

  // The invalid files are all invalid in different ways, and we want to cover
  // all the ways jpeg decoding can fail.  So, we want at least as many valid
  // images as invalid ones.
  ASSERT_GE(kValidImageCount, kInvalidFileCount);

  for (size_t i = 0; i < kInvalidFileCount; ++i) {
    GoogleString invalid_src_data;
    ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &invalid_src_data);
    GoogleString invalid_dest_data;

    GoogleString valid_src_data;
    ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename,
                        &valid_src_data);
    GoogleString valid_dest_data;

    ASSERT_FALSE(OptimizeJpeg(invalid_src_data, &invalid_dest_data,
                              &message_handler_));
    ASSERT_TRUE(OptimizeJpeg(valid_src_data, &valid_dest_data,
                             &message_handler_));

    // Diff the jpeg created by CreateOptimizedJpeg() with the one created
    // with a reinitialized JpegOptimizer.
    ASSERT_EQ(valid_dest_data, correctly_compressed.at(i));
  }
}

}  // namespace
