// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/webp_transcode/webp_decoder.h"

#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#include <stddef.h>
#include <stdint.h>

#include <memory>

#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/ios/ios_util.h"
#include "base/logging.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/path_service.h"
#include "base/strings/sys_string_conversions.h"
#include "build/build_config.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace webp_transcode {
namespace {

class WebpDecoderDelegate : public WebpDecoder::Delegate {
 public:
  WebpDecoderDelegate() : image_([[NSMutableData alloc] init]) {}

  NSData* GetImage() const { return image_; }

  // WebpDecoder::Delegate methods.
  MOCK_METHOD1(OnFinishedDecoding, void(bool success));
  MOCK_METHOD2(SetImageFeatures,
               void(size_t total_size, WebpDecoder::DecodedImageFormat format));
  void OnDataDecoded(NSData* data) override { [image_ appendData:data]; }

 private:
  virtual ~WebpDecoderDelegate() {}

  base::scoped_nsobject<NSMutableData> image_;
};

class WebpDecoderTest : public testing::Test {
 public:
  WebpDecoderTest()
      : delegate_(new WebpDecoderDelegate),
        decoder_(new WebpDecoder(delegate_.get())) {}

  NSData* LoadImage(const base::FilePath& filename) {
    base::FilePath path;
    PathService::Get(base::DIR_SOURCE_ROOT, &path);
    path = path.AppendASCII("components/test/data/webp_transcode")
               .Append(filename);
    return
        [NSData dataWithContentsOfFile:base::SysUTF8ToNSString(path.value())];
  }

  std::vector<uint8_t>* DecompressData(NSData* data,
                                       WebpDecoder::DecodedImageFormat format) {
    base::ScopedCFTypeRef<CGDataProviderRef> provider(
        CGDataProviderCreateWithCFData((CFDataRef)data));
    base::ScopedCFTypeRef<CGImageRef> image;
    switch (format) {
      case WebpDecoder::JPEG:
        image.reset(CGImageCreateWithJPEGDataProvider(
            provider, nullptr, false, kCGRenderingIntentDefault));
        break;
      case WebpDecoder::PNG:
        image.reset(CGImageCreateWithPNGDataProvider(
            provider, nullptr, false, kCGRenderingIntentDefault));
        break;
      case WebpDecoder::TIFF:
        ADD_FAILURE() << "Data already decompressed";
        return nil;
      case WebpDecoder::DECODED_FORMAT_COUNT:
        ADD_FAILURE() << "Unknown format";
        return nil;
    }
    size_t width = CGImageGetWidth(image);
    size_t height = CGImageGetHeight(image);
    base::ScopedCFTypeRef<CGColorSpaceRef> color_space(
        CGColorSpaceCreateDeviceRGB());
    size_t bytes_per_pixel = 4;
    size_t bytes_per_row = bytes_per_pixel * width;
    size_t bits_per_component = 8;
    std::vector<uint8_t>* result =
        new std::vector<uint8_t>(width * height * bytes_per_pixel, 0);
    base::ScopedCFTypeRef<CGContextRef> context(CGBitmapContextCreate(
        &result->front(), width, height, bits_per_component, bytes_per_row,
        color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big));
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
    // Check that someting has been written in |result|.
    std::vector<uint8_t> zeroes(width * height * bytes_per_pixel, 0);
    EXPECT_NE(0, memcmp(&result->front(), &zeroes.front(), zeroes.size()))
        << "Decompression failed.";
    return result;
  }

  // Compares data, allowing an averaged absolute difference of 1.
  bool CompareUncompressedData(const uint8_t* ptr_1,
                               const uint8_t* ptr_2,
                               size_t size) {
    uint64_t difference = 0;
    for (size_t i = 0; i < size; ++i) {
      // Casting to int to avoid overflow.
      int error = abs(int(ptr_1[i]) - int(ptr_2[i]));
      EXPECT_GE(difference + error, difference)
          << "Image difference too big (overflow).";
      difference += error;
    }
    double average_difference = double(difference) / double(size);
    DLOG(INFO) << "Average image difference: " << average_difference;
    return average_difference < 1.5;
  }

  bool CheckCompressedImagesEqual(NSData* data_1,
                                  NSData* data_2,
                                  WebpDecoder::DecodedImageFormat format) {
    std::unique_ptr<std::vector<uint8_t>> uncompressed_1(
        DecompressData(data_1, format));
    std::unique_ptr<std::vector<uint8_t>> uncompressed_2(
        DecompressData(data_2, format));
    if (uncompressed_1->size() != uncompressed_2->size()) {
      DLOG(ERROR) << "Image sizes don't match";
      return false;
    }
    return CompareUncompressedData(&uncompressed_1->front(),
                                   &uncompressed_2->front(),
                                   uncompressed_1->size());
  }

  bool CheckTiffImagesEqual(NSData* image_1, NSData* image_2) {
    if ([image_1 length] != [image_2 length]) {
      DLOG(ERROR) << "Image lengths don't match";
      return false;
    }
    // Compare headers.
    const size_t kHeaderSize = WebpDecoder::GetHeaderSize();
    NSData* header_1 = [image_1 subdataWithRange:NSMakeRange(0, kHeaderSize)];
    NSData* header_2 = [image_2 subdataWithRange:NSMakeRange(0, kHeaderSize)];
    if (!header_1 || !header_2)
      return false;
    if (![header_1 isEqualToData:header_2]) {
      DLOG(ERROR) << "Headers don't match.";
      return false;
    }
    return CompareUncompressedData(
        static_cast<const uint8_t*>([image_1 bytes]) + kHeaderSize,
        static_cast<const uint8_t*>([image_2 bytes]) + kHeaderSize,
        [image_1 length] - kHeaderSize);
  }

 protected:
  scoped_refptr<WebpDecoderDelegate> delegate_;
  scoped_refptr<WebpDecoder> decoder_;
};

}  // namespace

TEST_F(WebpDecoderTest, DecodeToJpeg) {
// TODO(droger): This test fails on iOS 9 x64 devices. http://crbug.com/523235
#if defined(OS_IOS) && defined(ARCH_CPU_ARM64) && !TARGET_IPHONE_SIMULATOR
  if (base::ios::IsRunningOnIOS9OrLater())
    return;
#endif
  // Load a WebP image from disk.
  base::scoped_nsobject<NSData> webp_image(
      [LoadImage(base::FilePath("test.webp")) retain]);
  ASSERT_TRUE(webp_image != nil);
  // Load reference image.
  base::scoped_nsobject<NSData> jpg_image(
      [LoadImage(base::FilePath("test.jpg")) retain]);
  ASSERT_TRUE(jpg_image != nil);
  // Convert to JPEG.
  EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1);
  EXPECT_CALL(*delegate_, SetImageFeatures(testing::_, WebpDecoder::JPEG))
      .Times(1);
  decoder_->OnDataReceived(webp_image);
  // Compare to reference image.
  EXPECT_TRUE(CheckCompressedImagesEqual(jpg_image, delegate_->GetImage(),
                                         WebpDecoder::JPEG));
}

TEST_F(WebpDecoderTest, DecodeToPng) {
// TODO(droger): This test fails on iOS 9 x64 devices. http://crbug.com/523235
#if defined(OS_IOS) && defined(ARCH_CPU_ARM64) && !TARGET_IPHONE_SIMULATOR
  if (base::ios::IsRunningOnIOS9OrLater())
    return;
#endif
  // Load a WebP image from disk.
  base::scoped_nsobject<NSData> webp_image(
      [LoadImage(base::FilePath("test_alpha.webp")) retain]);
  ASSERT_TRUE(webp_image != nil);
  // Load reference image.
  base::scoped_nsobject<NSData> png_image(
      [LoadImage(base::FilePath("test_alpha.png")) retain]);
  ASSERT_TRUE(png_image != nil);
  // Convert to PNG.
  EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1);
  EXPECT_CALL(*delegate_, SetImageFeatures(testing::_, WebpDecoder::PNG))
      .Times(1);
  decoder_->OnDataReceived(webp_image);
  // Compare to reference image.
  EXPECT_TRUE(CheckCompressedImagesEqual(png_image, delegate_->GetImage(),
                                         WebpDecoder::PNG));
}

TEST_F(WebpDecoderTest, DecodeToTiff) {
// TODO(droger): This test fails on iOS 9 x64 devices. http://crbug.com/523235
#if defined(OS_IOS) && defined(ARCH_CPU_ARM64) && !TARGET_IPHONE_SIMULATOR
  if (base::ios::IsRunningOnIOS9OrLater())
    return;
#endif
  // Load a WebP image from disk.
  base::scoped_nsobject<NSData> webp_image(
      [LoadImage(base::FilePath("test_small.webp")) retain]);
  ASSERT_TRUE(webp_image != nil);
  // Load reference image.
  base::scoped_nsobject<NSData> tiff_image(
      [LoadImage(base::FilePath("test_small.tiff")) retain]);
  ASSERT_TRUE(tiff_image != nil);
  // Convert to TIFF.
  EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1);
  EXPECT_CALL(*delegate_, SetImageFeatures([tiff_image length],
                                           WebpDecoder::TIFF)).Times(1);
  decoder_->OnDataReceived(webp_image);
  // Compare to reference image.
  EXPECT_TRUE(CheckTiffImagesEqual(tiff_image, delegate_->GetImage()));
}

TEST_F(WebpDecoderTest, StreamedDecode) {
// TODO(droger): This test fails on iOS 9 x64 devices. http://crbug.com/523235
#if defined(OS_IOS) && defined(ARCH_CPU_ARM64) && !TARGET_IPHONE_SIMULATOR
  if (base::ios::IsRunningOnIOS9OrLater())
    return;
#endif
  // Load a WebP image from disk.
  base::scoped_nsobject<NSData> webp_image(
      [LoadImage(base::FilePath("test.webp")) retain]);
  ASSERT_TRUE(webp_image != nil);
  // Load reference image.
  base::scoped_nsobject<NSData> jpg_image(
      [LoadImage(base::FilePath("test.jpg")) retain]);
  ASSERT_TRUE(jpg_image != nil);
  // Convert to JPEG in chunks.
  EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1);
  EXPECT_CALL(*delegate_, SetImageFeatures(testing::_, WebpDecoder::JPEG))
      .Times(1);
  const size_t kChunkSize = 10;
  unsigned int num_chunks = 0;
  while ([webp_image length] > kChunkSize) {
    base::scoped_nsobject<NSData> chunk(
        [[webp_image subdataWithRange:NSMakeRange(0, kChunkSize)] retain]);
    decoder_->OnDataReceived(chunk);
    webp_image.reset([[webp_image
        subdataWithRange:NSMakeRange(kChunkSize, [webp_image length] -
                                                     kChunkSize)] retain]);
    ++num_chunks;
  }
  if ([webp_image length] > 0u) {
    decoder_->OnDataReceived(webp_image);
    ++num_chunks;
  }
  ASSERT_GT(num_chunks, 3u) << "Not enough chunks";
  // Compare to reference image.
  EXPECT_TRUE(CheckCompressedImagesEqual(jpg_image, delegate_->GetImage(),
                                         WebpDecoder::JPEG));
}

TEST_F(WebpDecoderTest, InvalidFormat) {
  EXPECT_CALL(*delegate_, OnFinishedDecoding(false)).Times(1);
  const char dummy_image[] = "(>'-')> <('-'<) ^('-')^ <('-'<) (>'-')>";
  base::scoped_nsobject<NSData> data(
      [[NSData alloc] initWithBytes:dummy_image length:arraysize(dummy_image)]);
  decoder_->OnDataReceived(data);
  EXPECT_EQ(0u, [delegate_->GetImage() length]);
}

TEST_F(WebpDecoderTest, DecodeAborted) {
  EXPECT_CALL(*delegate_, OnFinishedDecoding(false)).Times(1);
  decoder_->Stop();
  EXPECT_EQ(0u, [delegate_->GetImage() length]);
}

}  // namespace webp_transcode
