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

#include <math.h>
#include <cstdlib>
#include <cstdint>
#include <vector>

#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/message_handler.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/stdio_file_system.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/image/read_image.h"
#include "pagespeed/kernel/image/scanline_interface.h"
#include "pagespeed/kernel/image/scanline_utils.h"


namespace pagespeed {

namespace {

const double kMaxPSNR = 99.0;
const int kIndexAlpha = 3;
const uint8_t kAlphaTransparent = 0;

// Definition of Peak-Signal-to-Noise-Ratio (PSNR):
// http://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
//
// The implementation is similar to
// third_party/libwebp/tests/check_psnr.cc.
// However, this implementation supports image with different number of
// channels. It also allows padding at the end of scanlines.
double ComputePSNR(const uint8_t* pixels1, const uint8_t* pixels2,
                   int width, int height, int num_channels, int stride) {
  double error = 0.0;
  for (int y = 0; y < height; ++y) {
    for (int x = 0; x < width; ++x) {
      for (int ch = 0; ch < num_channels; ++ch) {
        double alpha_scaling1 = 0, alpha_scaling2 = 0;
        if (ch != kIndexAlpha && num_channels > 3) {
          alpha_scaling1 = pixels1[kIndexAlpha] / 255.0;
          alpha_scaling2 = pixels2[kIndexAlpha] / 255.0;
        }
        int index = y * stride + (x * num_channels + ch);
        double dif = static_cast<double>(pixels1[index] * alpha_scaling1) -
                     static_cast<double>(pixels2[index] * alpha_scaling2);
        error += dif * dif;
      }
    }
  }
  error /= (height * width * num_channels);
  return (error > 0.0) ? 10.0 * log10(255.0 * 255.0 / error) : kMaxPSNR;
}

}  // namespace

namespace image_compression {

using net_instaweb::MessageHandler;

bool ReadFile(const GoogleString& file_name,
              GoogleString* content) {
  content->clear();
  net_instaweb::StdioFileSystem file_system;
  net_instaweb::MockMessageHandler message_handler(new net_instaweb::NullMutex);
  net_instaweb::StringWriter writer(content);
  return(file_system.ReadFile(file_name.c_str(), &writer, &message_handler));
}

bool ReadTestFile(const GoogleString& path,
                  const char* name,
                  const char* extension,
                  GoogleString* content) {
  content->clear();
  GoogleString file_name = net_instaweb::StrCat(net_instaweb::GTestSrcDir(),
       kTestRootDir, path, name, ".", extension);
  return ReadFile(file_name, content);
}

bool ReadTestFileWithExt(const GoogleString& path,
                         const char* name_with_extension,
                         GoogleString* content) {
  GoogleString file_name = net_instaweb::StrCat(net_instaweb::GTestSrcDir(),
       kTestRootDir, path, name_with_extension);
  return ReadFile(file_name, content);
}

void DecodeAndCompareImages(
    pagespeed::image_compression::ImageFormat image_format1,
    const void* image_buffer1,
    size_t buffer_length1,
    pagespeed::image_compression::ImageFormat image_format2,
    const void* image_buffer2,
    size_t buffer_length2,
    bool ignore_transparent_rgb,
    MessageHandler* message_handler) {
  DecodeAndCompareImagesByPSNR(image_format1, image_buffer1, buffer_length1,
                               image_format2, image_buffer2, buffer_length2,
                               kMaxPSNR, ignore_transparent_rgb,
                               true /* expand colors */,
                               message_handler);
}

void DecodeAndCompareImagesByPSNR(
    pagespeed::image_compression::ImageFormat image_format1,
    const void* image_buffer1,
    size_t buffer_length1,
    pagespeed::image_compression::ImageFormat image_format2,
    const void* image_buffer2,
    size_t buffer_length2,
    double min_psnr,
    bool ignore_transparent_rgb,
    bool expand_colors,
    MessageHandler* message_handler) {
  uint8_t* pixels1 = NULL;
  uint8_t* pixels2 = NULL;
  PixelFormat pixel_format1, pixel_format2;
  size_t width1, height1, stride1, width2, height2, stride2;

  // Decode the images.
  ASSERT_TRUE(ReadImage(image_format1, image_buffer1, buffer_length1,
                        reinterpret_cast<void**>(&pixels1),
                        &pixel_format1, &width1, &height1, &stride1,
                        message_handler));
  ASSERT_TRUE(ReadImage(image_format2, image_buffer2, buffer_length2,
                        reinterpret_cast<void**>(&pixels2),
                        &pixel_format2, &width2, &height2, &stride2,
                        message_handler));

  // Verify that the pixel format and sizes are the same.
  ASSERT_EQ(width1, width2);
  ASSERT_EQ(height1, height2);
  if (!expand_colors) {
    ASSERT_EQ(pixel_format1, pixel_format2);
    ASSERT_EQ(stride1, stride2);
  }

  CompareImageRegionsByPSNR(pixels1, pixel_format1, stride1, 0, 0,
                            pixels2, pixel_format2, stride2, 0, 0,
                            width1, height1, min_psnr, ignore_transparent_rgb,
                            expand_colors, message_handler);

  free(pixels1);
  free(pixels2);
}

void CompareImageReaders(ScanlineReaderInterface* reader1,
                         ScanlineReaderInterface* reader2) {
  ASSERT_NE(reinterpret_cast<ScanlineReaderInterface*>(NULL), reader1);
  ASSERT_NE(reinterpret_cast<ScanlineReaderInterface*>(NULL), reader2);
  ASSERT_EQ(reader1->GetPixelFormat(), reader2->GetPixelFormat());
  ASSERT_EQ(reader1->GetImageHeight(), reader2->GetImageHeight());
  ASSERT_EQ(reader1->GetImageWidth(), reader2->GetImageWidth());
  ASSERT_EQ(reader1->GetBytesPerScanline(), reader2->GetBytesPerScanline());

  while (reader1->HasMoreScanLines() && reader2->HasMoreScanLines()) {
    uint8_t* scanline1 = NULL;
    uint8_t* scanline2 = NULL;
    ASSERT_TRUE(reader1->ReadNextScanline(
        reinterpret_cast<void**>(&scanline1)));
    ASSERT_TRUE(reader2->ReadNextScanline(
        reinterpret_cast<void**>(&scanline2)));
    EXPECT_EQ(0, memcmp(scanline1, scanline2, reader1->GetBytesPerScanline()));
  }

  // Make sure both readers have exhausted all of the scanlines.
  EXPECT_FALSE(reader1->HasMoreScanLines());
  EXPECT_FALSE(reader2->HasMoreScanLines());
}

void ComparePixelsByPSNR(const uint8_t* image1, const uint8_t* image2,
                         PixelFormat format, int num_rows, int num_cols,
                         double min_psnr, bool ignore_transparent_rgb,
                         MessageHandler* handler) {
  int num_channels = GetNumChannelsFromPixelFormat(format, handler);
  int bytes_per_line = num_channels * num_cols;
  int bytes_per_image = bytes_per_line * num_rows;
  if (min_psnr >= kMaxPSNR) {
    // Verify that all of the pixels are exactly the same.
    if (!ignore_transparent_rgb || format != RGBA_8888) {
      EXPECT_EQ(0, memcmp(image1, image2, bytes_per_image));
    } else {
      // To ignore transparent pixels, we have to check the pixels one by one.
      for (int row = 0; row < num_rows; ++row) {
        for (int col = 0; col < num_cols; ++col) {
          int ch = 0;
          if (image1[row * bytes_per_line + col * 4 + 3] == 0) {
            // Skip checking RGB when alpha is 0, still test alpha itself.
            ch = 3;  // Index of alpha channel in RGBA_8888.
          }

          for (; ch < 4; ++ch) {
            int index = row * bytes_per_line + col * 4 + ch;
            EXPECT_EQ(image1[index], image2[index])
                << "  row: " << row
                << "  col: " << col
                << "  ch: " << ch
                << "  index: " << index;
          }
        }
      }
    }
  } else {
    double psnr =
        ComputePSNR(image1, image2, num_cols, num_rows, num_channels,
                    bytes_per_line);
    EXPECT_LE(min_psnr, psnr);
  }
}

void CompareImageRegionsByPSNR(const uint8_t* image1, PixelFormat format1,
                               int bytes_per_row1, int col1, int row1,
                               const uint8_t* image2, PixelFormat format2,
                               int bytes_per_row2, int col2, int row2,
                               int num_cols, int num_rows, double min_psnr,
                               bool ignore_transparent_rgb,
                               bool expand_colors, MessageHandler* handler) {
  ASSERT_TRUE(format1 != UNSUPPORTED && format2 != UNSUPPORTED);
  const int num_channels1 =
    GetNumChannelsFromPixelFormat(format1, handler);
  const int num_channels2 =
    GetNumChannelsFromPixelFormat(format2, handler);

  PixelFormat format;
  int num_channels;
  if (num_channels1 >= num_channels2) {
    format = format1;
    num_channels = num_channels1;
  } else {
    format = format2;
    num_channels = num_channels2;
  }
  int bytes_per_line = num_cols * num_channels;

  int bytes_per_image = bytes_per_line * num_rows;
  net_instaweb::scoped_array<uint8_t>
      image_buffer1(new uint8_t[bytes_per_image]);
  net_instaweb::scoped_array<uint8_t>
      image_buffer2(new uint8_t[bytes_per_image]);
  ASSERT_TRUE(image_buffer1 != NULL && image_buffer2 != NULL);

  memset(image_buffer1.get(), 0, bytes_per_image);
  memset(image_buffer2.get(), 0, bytes_per_image);

  bool should_expand_colors = (format1 != format2 && expand_colors);
  for (int row = 0; row < num_rows; ++row) {
    const uint8_t* src1 = image1 + (row + row1) * bytes_per_row1;
    uint8_t* dest1 = image_buffer1.get() + row * bytes_per_line;
    const uint8_t* src2 = image2 + (row + row2) * bytes_per_row2;
    uint8_t* dest2 = image_buffer2.get() + row * bytes_per_line;

    if (should_expand_colors) {
      // Expand and copy colors.
      ASSERT_TRUE(ExpandPixelFormat(num_cols, format1, col1, src1, format, 0,
                                    dest1, handler));
      ASSERT_TRUE(ExpandPixelFormat(num_cols, format2, col2, src2, format, 0,
                                    dest2, handler));
    } else {
      memcpy(dest1, src1 + col1 * num_channels1, bytes_per_line);
      memcpy(dest2, src2 + col2 * num_channels2, bytes_per_line);
    }
  }

  ComparePixelsByPSNR(image_buffer1.get(), image_buffer2.get(),
                      format, num_rows, num_cols, min_psnr,
                      ignore_transparent_rgb, handler);
}

void CompareImageRegions(const uint8_t* image1, PixelFormat format1,
                         int bytes_per_row1, int col1, int row1,
                         const uint8_t* image2, PixelFormat format2,
                         int bytes_per_row2, int col2, int row2,
                         int num_cols, int num_rows, MessageHandler* handler) {
  CompareImageRegionsByPSNR(image1, format1, bytes_per_row1, col1, row1,
                            image2, format2, bytes_per_row2, col2, row2,
                            num_cols, num_rows, kMaxPSNR,
                            false,  // ignore_transparent_rgb
                            true,  // Expand colors
                            handler);
}

void SynthesizeImage(int width, int height, int bytes_per_line,
                     int num_channels, const uint8_t* seed_value,
                     const int* delta_x, const int* delta_y, uint8_t* image) {
  ASSERT_TRUE(image != NULL);
  ASSERT_GT(width, 0);
  ASSERT_GT(height, 0);
  ASSERT_GE(bytes_per_line, width);

  net_instaweb::scoped_array<uint8_t> current_value(new uint8_t[num_channels]);
  memcpy(current_value.get(), seed_value,
         num_channels * sizeof(seed_value[0]));

  for (int y = 0; y < height; ++y) {
    uint8_t* pixel = image + y * bytes_per_line;
    for (int x = 0; x < width; ++x) {
      for (int ch = 0; ch < num_channels; ++ch) {
        pixel[ch] = current_value[ch];
        current_value[ch] += delta_x[ch];
      }
      pixel += num_channels;
    }
    // Compute the value for the first pixel in the next line. The next line
    // has values increased from those of the current line by delta_y.
    for (int ch = 0; ch < num_channels; ++ch) {
      current_value[ch] = image[y * bytes_per_line + ch] + delta_y[ch];
    }
  }
}

// Returns true if 2 animated images are identical.
bool CompareAnimatedImages(const GoogleString& expected_image_filename,
                           const GoogleString& actual_image_content,
                           MessageHandler* message_handler) {
  // TODO(vchudnov): Fill this in.
  return true;
}

}  // namespace image_compression

}  // namespace pagespeed
