/*
 * libjingle
 * Copyright 2014 Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <string>

#include "libyuv/convert.h"
#include "libyuv/convert_from.h"
#include "libyuv/convert_from_argb.h"
#include "libyuv/mjpeg_decoder.h"
#include "libyuv/planar_functions.h"
#include "talk/media/base/testutils.h"
#include "talk/media/base/videocommon.h"
#include "webrtc/base/flags.h"
#include "webrtc/base/gunit.h"
#include "webrtc/base/scoped_ptr.h"

// Undefine macros for the windows build.
#undef max
#undef min

using cricket::DumpPlanarYuvTestImage;

DEFINE_bool(planarfunctions_dump, false,
    "whether to write out scaled images for inspection");
DEFINE_int(planarfunctions_repeat, 1,
    "how many times to perform each scaling operation (for perf testing)");

namespace cricket {

// Number of testing colors in each color channel.
static const int kTestingColorChannelResolution = 6;

// The total number of testing colors
// kTestingColorNum = kTestingColorChannelResolution^3;
static const int kTestingColorNum = kTestingColorChannelResolution *
    kTestingColorChannelResolution * kTestingColorChannelResolution;

static const int kWidth = 1280;
static const int kHeight = 720;
static const int kAlignment = 16;

class PlanarFunctionsTest : public testing::TestWithParam<int> {
 protected:
  PlanarFunctionsTest() : dump_(false), repeat_(1) {
    InitializeColorBand();
  }

  virtual void SetUp() {
    dump_ = FLAG_planarfunctions_dump;
    repeat_ = FLAG_planarfunctions_repeat;
  }

  // Initialize the color band for testing.
  void InitializeColorBand() {
    testing_color_y_.reset(new uint8[kTestingColorNum]);
    testing_color_u_.reset(new uint8[kTestingColorNum]);
    testing_color_v_.reset(new uint8[kTestingColorNum]);
    testing_color_r_.reset(new uint8[kTestingColorNum]);
    testing_color_g_.reset(new uint8[kTestingColorNum]);
    testing_color_b_.reset(new uint8[kTestingColorNum]);
    int color_counter = 0;
    for (int i = 0; i < kTestingColorChannelResolution; ++i) {
      uint8 color_r = static_cast<uint8>(
          i * 255 / (kTestingColorChannelResolution - 1));
      for (int j = 0; j < kTestingColorChannelResolution; ++j) {
        uint8 color_g = static_cast<uint8>(
            j * 255 / (kTestingColorChannelResolution - 1));
        for (int k = 0; k < kTestingColorChannelResolution; ++k) {
          uint8 color_b = static_cast<uint8>(
              k * 255 / (kTestingColorChannelResolution - 1));
          testing_color_r_[color_counter] = color_r;
          testing_color_g_[color_counter] = color_g;
          testing_color_b_[color_counter] = color_b;
           // Converting the testing RGB colors to YUV colors.
          ConvertRgbPixel(color_r, color_g, color_b,
                          &(testing_color_y_[color_counter]),
                          &(testing_color_u_[color_counter]),
                          &(testing_color_v_[color_counter]));
          ++color_counter;
        }
      }
    }
  }
  // Simple and slow RGB->YUV conversion. From NTSC standard, c/o Wikipedia.
  // (from lmivideoframe_unittest.cc)
  void ConvertRgbPixel(uint8 r, uint8 g, uint8 b,
                       uint8* y, uint8* u, uint8* v) {
    *y = ClampUint8(.257 * r + .504 * g + .098 * b + 16);
    *u = ClampUint8(-.148 * r - .291 * g + .439 * b + 128);
    *v = ClampUint8(.439 * r - .368 * g - .071 * b + 128);
  }

  uint8 ClampUint8(double value) {
    value = std::max(0., std::min(255., value));
    uint8 uint8_value = static_cast<uint8>(value);
    return uint8_value;
  }

  // Generate a Red-Green-Blue inter-weaving chessboard-like
  // YUV testing image (I420/I422/I444).
  // The pattern looks like c0 c1 c2 c3 ...
  //                        c1 c2 c3 c4 ...
  //                        c2 c3 c4 c5 ...
  //                        ...............
  // The size of each chrome block is (block_size) x (block_size).
  uint8* CreateFakeYuvTestingImage(int height, int width, int block_size,
                                   libyuv::JpegSubsamplingType subsample_type,
                                   uint8* &y_pointer,
                                   uint8* &u_pointer,
                                   uint8* &v_pointer) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }
    int y_size = height * width;
    int u_size, v_size;
    int vertical_sample_ratio = 1, horizontal_sample_ratio = 1;
    switch (subsample_type) {
      case libyuv::kJpegYuv420:
        u_size = ((height + 1) >> 1) * ((width + 1) >> 1);
        v_size = u_size;
        vertical_sample_ratio = 2, horizontal_sample_ratio = 2;
        break;
      case libyuv::kJpegYuv422:
        u_size = height * ((width + 1) >> 1);
        v_size = u_size;
        vertical_sample_ratio = 1, horizontal_sample_ratio = 2;
        break;
      case libyuv::kJpegYuv444:
        v_size = u_size = y_size;
        vertical_sample_ratio = 1, horizontal_sample_ratio = 1;
        break;
      case libyuv::kJpegUnknown:
      default:
        return NULL;
        break;
    }
    uint8* image_pointer = new uint8[y_size + u_size + v_size + kAlignment];
    y_pointer = ALIGNP(image_pointer, kAlignment);
    u_pointer = ALIGNP(&image_pointer[y_size], kAlignment);
    v_pointer = ALIGNP(&image_pointer[y_size + u_size], kAlignment);
    uint8* current_y_pointer = y_pointer;
    uint8* current_u_pointer = u_pointer;
    uint8* current_v_pointer = v_pointer;
    for (int j = 0; j < height; ++j) {
      for (int i = 0; i < width; ++i) {
        int color = ((i / block_size) + (j / block_size)) % kTestingColorNum;
        *(current_y_pointer++) = testing_color_y_[color];
        if (i % horizontal_sample_ratio == 0 &&
            j % vertical_sample_ratio == 0) {
          *(current_u_pointer++) = testing_color_u_[color];
          *(current_v_pointer++) = testing_color_v_[color];
        }
      }
    }
    return image_pointer;
  }

  // Generate a Red-Green-Blue inter-weaving chessboard-like
  // YUY2/UYVY testing image.
  // The pattern looks like c0 c1 c2 c3 ...
  //                        c1 c2 c3 c4 ...
  //                        c2 c3 c4 c5 ...
  //                        ...............
  // The size of each chrome block is (block_size) x (block_size).
  uint8* CreateFakeInterleaveYuvTestingImage(
      int height, int width, int block_size,
      uint8* &yuv_pointer, FourCC fourcc_type) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }
    if (fourcc_type != FOURCC_YUY2 && fourcc_type != FOURCC_UYVY) {
      LOG(LS_ERROR) << "Format " << static_cast<int>(fourcc_type)
                    << " is not supported.";
      return NULL;
    }
    // Regularize the width of the output to be even.
    int awidth = (width + 1) & ~1;

    uint8* image_pointer = new uint8[2 * height * awidth + kAlignment];
    yuv_pointer = ALIGNP(image_pointer, kAlignment);
    uint8* current_yuv_pointer = yuv_pointer;
    switch (fourcc_type) {
      case FOURCC_YUY2: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < awidth; i += 2, current_yuv_pointer += 4) {
            int color1 = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            int color2 = (((i + 1) / block_size) + (j / block_size)) %
                kTestingColorNum;
            current_yuv_pointer[0] = testing_color_y_[color1];
            if (i < width) {
              current_yuv_pointer[1] = static_cast<uint8>(
                  (static_cast<uint32>(testing_color_u_[color1]) +
                  static_cast<uint32>(testing_color_u_[color2])) / 2);
              current_yuv_pointer[2] = testing_color_y_[color2];
              current_yuv_pointer[3] = static_cast<uint8>(
                  (static_cast<uint32>(testing_color_v_[color1]) +
                  static_cast<uint32>(testing_color_v_[color2])) / 2);
            } else {
              current_yuv_pointer[1] = testing_color_u_[color1];
              current_yuv_pointer[2] = 0;
              current_yuv_pointer[3] = testing_color_v_[color1];
            }
          }
        }
        break;
      }
      case FOURCC_UYVY: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < awidth; i += 2, current_yuv_pointer += 4) {
            int color1 = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            int color2 = (((i + 1) / block_size) + (j / block_size)) %
                kTestingColorNum;
            if (i < width) {
              current_yuv_pointer[0] = static_cast<uint8>(
                  (static_cast<uint32>(testing_color_u_[color1]) +
                  static_cast<uint32>(testing_color_u_[color2])) / 2);
              current_yuv_pointer[1] = testing_color_y_[color1];
              current_yuv_pointer[2] = static_cast<uint8>(
                  (static_cast<uint32>(testing_color_v_[color1]) +
                  static_cast<uint32>(testing_color_v_[color2])) / 2);
              current_yuv_pointer[3] = testing_color_y_[color2];
            } else {
              current_yuv_pointer[0] = testing_color_u_[color1];
              current_yuv_pointer[1] = testing_color_y_[color1];
              current_yuv_pointer[2] = testing_color_v_[color1];
              current_yuv_pointer[3] = 0;
            }
          }
        }
        break;
      }
    }
    return image_pointer;
  }

  // Generate a Red-Green-Blue inter-weaving chessboard-like
  // NV12 testing image.
  // (Note: No interpolation is used.)
  // The pattern looks like c0 c1 c2 c3 ...
  //                        c1 c2 c3 c4 ...
  //                        c2 c3 c4 c5 ...
  //                        ...............
  // The size of each chrome block is (block_size) x (block_size).
  uint8* CreateFakeNV12TestingImage(int height, int width, int block_size,
      uint8* &y_pointer, uint8* &uv_pointer) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }

    uint8* image_pointer = new uint8[height * width +
        ((height + 1) / 2) * ((width + 1) / 2) * 2 + kAlignment];
    y_pointer = ALIGNP(image_pointer, kAlignment);
    uv_pointer = y_pointer + height * width;
    uint8* current_uv_pointer = uv_pointer;
    uint8* current_y_pointer = y_pointer;
    for (int j = 0; j < height; ++j) {
      for (int i = 0; i < width; ++i) {
        int color = ((i / block_size) + (j / block_size)) %
            kTestingColorNum;
        *(current_y_pointer++) = testing_color_y_[color];
      }
      if (j % 2 == 0) {
        for (int i = 0; i < width; i += 2, current_uv_pointer += 2) {
          int color = ((i / block_size) + (j / block_size)) %
              kTestingColorNum;
          current_uv_pointer[0] = testing_color_u_[color];
          current_uv_pointer[1] = testing_color_v_[color];
        }
      }
    }
    return image_pointer;
  }

  // Generate a Red-Green-Blue inter-weaving chessboard-like
  // M420 testing image.
  // (Note: No interpolation is used.)
  // The pattern looks like c0 c1 c2 c3 ...
  //                        c1 c2 c3 c4 ...
  //                        c2 c3 c4 c5 ...
  //                        ...............
  // The size of each chrome block is (block_size) x (block_size).
  uint8* CreateFakeM420TestingImage(
      int height, int width, int block_size, uint8* &m420_pointer) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }

    uint8* image_pointer = new uint8[height * width +
        ((height + 1) / 2) * ((width + 1) / 2) * 2 + kAlignment];
    m420_pointer = ALIGNP(image_pointer, kAlignment);
    uint8* current_m420_pointer = m420_pointer;
    for (int j = 0; j < height; ++j) {
      for (int i = 0; i < width; ++i) {
        int color = ((i / block_size) + (j / block_size)) %
            kTestingColorNum;
        *(current_m420_pointer++) = testing_color_y_[color];
      }
      if (j % 2 == 1) {
        for (int i = 0; i < width; i += 2, current_m420_pointer += 2) {
          int color = ((i / block_size) + ((j - 1) / block_size)) %
              kTestingColorNum;
          current_m420_pointer[0] = testing_color_u_[color];
          current_m420_pointer[1] = testing_color_v_[color];
        }
      }
    }
    return image_pointer;
  }

  // Generate a Red-Green-Blue inter-weaving chessboard-like
  // ARGB/ABGR/RAW/BG24 testing image.
  // The pattern looks like c0 c1 c2 c3 ...
  //                        c1 c2 c3 c4 ...
  //                        c2 c3 c4 c5 ...
  //                        ...............
  // The size of each chrome block is (block_size) x (block_size).
  uint8* CreateFakeArgbTestingImage(int height, int width, int block_size,
                                    uint8* &argb_pointer, FourCC fourcc_type) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }
    uint8* image_pointer = NULL;
    if (fourcc_type == FOURCC_ABGR || fourcc_type == FOURCC_BGRA ||
        fourcc_type == FOURCC_ARGB) {
      image_pointer = new uint8[height * width * 4 + kAlignment];
    } else if (fourcc_type == FOURCC_RAW || fourcc_type == FOURCC_24BG) {
      image_pointer = new uint8[height * width * 3 + kAlignment];
    } else {
      LOG(LS_ERROR) << "Format " << static_cast<int>(fourcc_type)
                    << " is not supported.";
      return NULL;
    }
    argb_pointer = ALIGNP(image_pointer, kAlignment);
    uint8* current_pointer = argb_pointer;
    switch (fourcc_type) {
      case FOURCC_ARGB: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < width; ++i) {
            int color = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            *(current_pointer++) = testing_color_b_[color];
            *(current_pointer++) = testing_color_g_[color];
            *(current_pointer++) = testing_color_r_[color];
            *(current_pointer++) = 255;
          }
        }
        break;
      }
      case FOURCC_ABGR: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < width; ++i) {
            int color = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            *(current_pointer++) = testing_color_r_[color];
            *(current_pointer++) = testing_color_g_[color];
            *(current_pointer++) = testing_color_b_[color];
            *(current_pointer++) = 255;
          }
        }
        break;
      }
      case FOURCC_BGRA: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < width; ++i) {
            int color = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            *(current_pointer++) = 255;
            *(current_pointer++) = testing_color_r_[color];
            *(current_pointer++) = testing_color_g_[color];
            *(current_pointer++) = testing_color_b_[color];
           }
        }
        break;
      }
      case FOURCC_24BG: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < width; ++i) {
            int color = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            *(current_pointer++) = testing_color_b_[color];
            *(current_pointer++) = testing_color_g_[color];
            *(current_pointer++) = testing_color_r_[color];
          }
        }
        break;
      }
      case FOURCC_RAW: {
        for (int j = 0; j < height; ++j) {
          for (int i = 0; i < width; ++i) {
            int color = ((i / block_size) + (j / block_size)) %
                kTestingColorNum;
            *(current_pointer++) = testing_color_r_[color];
            *(current_pointer++) = testing_color_g_[color];
            *(current_pointer++) = testing_color_b_[color];
          }
        }
        break;
      }
      default: {
        LOG(LS_ERROR) << "Format " << static_cast<int>(fourcc_type)
                      << " is not supported.";
      }
    }
    return image_pointer;
  }

  // Check if two memory chunks are equal.
  // (tolerate MSE errors within a threshold).
  static bool IsMemoryEqual(const uint8* ibuf, const uint8* obuf,
                            int osize, double average_error) {
    double sse = cricket::ComputeSumSquareError(ibuf, obuf, osize);
    double error = sse / osize;  // Mean Squared Error.
    double PSNR = cricket::ComputePSNR(sse, osize);
    LOG(LS_INFO) << "Image MSE: "  << error << " Image PSNR: " << PSNR
                 << " First Diff Byte: " << FindDiff(ibuf, obuf, osize);
    return (error < average_error);
  }

  // Returns the index of the first differing byte. Easier to debug than memcmp.
  static int FindDiff(const uint8* buf1, const uint8* buf2, int len) {
    int i = 0;
    while (i < len && buf1[i] == buf2[i]) {
      i++;
    }
    return (i < len) ? i : -1;
  }

  // Dump the result image (ARGB format).
  void DumpArgbImage(const uint8* obuf, int width, int height) {
    DumpPlanarArgbTestImage(GetTestName(), obuf, width, height);
  }

  // Dump the result image (YUV420 format).
  void DumpYuvImage(const uint8* obuf, int width, int height) {
    DumpPlanarYuvTestImage(GetTestName(), obuf, width, height);
  }

  std::string GetTestName() {
    const testing::TestInfo* const test_info =
        testing::UnitTest::GetInstance()->current_test_info();
    std::string test_name(test_info->name());
    return test_name;
  }

  bool dump_;
  int repeat_;

  // Y, U, V and R, G, B channels of testing colors.
  rtc::scoped_ptr<uint8[]> testing_color_y_;
  rtc::scoped_ptr<uint8[]> testing_color_u_;
  rtc::scoped_ptr<uint8[]> testing_color_v_;
  rtc::scoped_ptr<uint8[]> testing_color_r_;
  rtc::scoped_ptr<uint8[]> testing_color_g_;
  rtc::scoped_ptr<uint8[]> testing_color_b_;
};

TEST_F(PlanarFunctionsTest, I420Copy) {
  uint8 *y_pointer = NULL, *u_pointer = NULL, *v_pointer = NULL;
  int y_pitch = kWidth;
  int u_pitch = (kWidth + 1) >> 1;
  int v_pitch = (kWidth + 1) >> 1;
  int y_size = kHeight * kWidth;
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1);
  int block_size = 3;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> yuv_input(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
                                libyuv::kJpegYuv420,
                                y_pointer, u_pointer, v_pointer));
  // Allocate space for the output image.
  rtc::scoped_ptr<uint8[]> yuv_output(
      new uint8[I420_SIZE(kHeight, kWidth) + kAlignment]);
  uint8 *y_output_pointer = ALIGNP(yuv_output.get(), kAlignment);
  uint8 *u_output_pointer = y_output_pointer + y_size;
  uint8 *v_output_pointer = u_output_pointer + uv_size;

  for (int i = 0; i < repeat_; ++i) {
  libyuv::I420Copy(y_pointer, y_pitch,
                   u_pointer, u_pitch,
                   v_pointer, v_pitch,
                   y_output_pointer, y_pitch,
                   u_output_pointer, u_pitch,
                   v_output_pointer, v_pitch,
                   kWidth, kHeight);
  }

  // Expect the copied frame to be exactly the same.
  EXPECT_TRUE(IsMemoryEqual(y_output_pointer, y_pointer,
      I420_SIZE(kHeight, kWidth), 1.e-6));

  if (dump_) { DumpYuvImage(y_output_pointer, kWidth, kHeight); }
}

TEST_F(PlanarFunctionsTest, I422ToI420) {
  uint8 *y_pointer = NULL, *u_pointer = NULL, *v_pointer = NULL;
  int y_pitch = kWidth;
  int u_pitch = (kWidth + 1) >> 1;
  int v_pitch = (kWidth + 1) >> 1;
  int y_size = kHeight * kWidth;
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1);
  int block_size = 2;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> yuv_input(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
                                libyuv::kJpegYuv422,
                                y_pointer, u_pointer, v_pointer));
  // Allocate space for the output image.
  rtc::scoped_ptr<uint8[]> yuv_output(
      new uint8[I420_SIZE(kHeight, kWidth) + kAlignment]);
  uint8 *y_output_pointer = ALIGNP(yuv_output.get(), kAlignment);
  uint8 *u_output_pointer = y_output_pointer + y_size;
  uint8 *v_output_pointer = u_output_pointer + uv_size;
  // Generate the expected output.
  uint8 *y_expected_pointer = NULL, *u_expected_pointer = NULL,
        *v_expected_pointer = NULL;
  rtc::scoped_ptr<uint8[]> yuv_output_expected(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
          libyuv::kJpegYuv420,
          y_expected_pointer, u_expected_pointer, v_expected_pointer));

  for (int i = 0; i < repeat_; ++i) {
  libyuv::I422ToI420(y_pointer, y_pitch,
                     u_pointer, u_pitch,
                     v_pointer, v_pitch,
                     y_output_pointer, y_pitch,
                     u_output_pointer, u_pitch,
                     v_output_pointer, v_pitch,
                     kWidth, kHeight);
  }

  // Compare the output frame with what is expected; expect exactly the same.
  // Note: MSE should be set to a larger threshold if an odd block width
  // is used, since the conversion will be lossy.
  EXPECT_TRUE(IsMemoryEqual(y_output_pointer, y_expected_pointer,
      I420_SIZE(kHeight, kWidth), 1.e-6));

  if (dump_) { DumpYuvImage(y_output_pointer, kWidth, kHeight); }
}

TEST_P(PlanarFunctionsTest, M420ToI420) {
  // Get the unalignment offset
  int unalignment = GetParam();
  uint8 *m420_pointer = NULL;
  int y_pitch = kWidth;
  int m420_pitch = kWidth;
  int u_pitch = (kWidth + 1) >> 1;
  int v_pitch = (kWidth + 1) >> 1;
  int y_size = kHeight * kWidth;
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1);
  int block_size = 2;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> yuv_input(
      CreateFakeM420TestingImage(kHeight, kWidth, block_size, m420_pointer));
  // Allocate space for the output image.
  rtc::scoped_ptr<uint8[]> yuv_output(
      new uint8[I420_SIZE(kHeight, kWidth) + kAlignment + unalignment]);
  uint8 *y_output_pointer = ALIGNP(yuv_output.get(), kAlignment) + unalignment;
  uint8 *u_output_pointer = y_output_pointer + y_size;
  uint8 *v_output_pointer = u_output_pointer + uv_size;
  // Generate the expected output.
  uint8 *y_expected_pointer = NULL, *u_expected_pointer = NULL,
        *v_expected_pointer = NULL;
  rtc::scoped_ptr<uint8[]> yuv_output_expected(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
          libyuv::kJpegYuv420,
          y_expected_pointer, u_expected_pointer, v_expected_pointer));

  for (int i = 0; i < repeat_; ++i) {
  libyuv::M420ToI420(m420_pointer, m420_pitch,
                     y_output_pointer, y_pitch,
                     u_output_pointer, u_pitch,
                     v_output_pointer, v_pitch,
                     kWidth, kHeight);
  }
  // Compare the output frame with what is expected; expect exactly the same.
  // Note: MSE should be set to a larger threshold if an odd block width
  // is used, since the conversion will be lossy.
  EXPECT_TRUE(IsMemoryEqual(y_output_pointer, y_expected_pointer,
      I420_SIZE(kHeight, kWidth), 1.e-6));

  if (dump_) { DumpYuvImage(y_output_pointer, kWidth, kHeight); }
}

TEST_P(PlanarFunctionsTest, NV12ToI420) {
  // Get the unalignment offset
  int unalignment = GetParam();
  uint8 *y_pointer = NULL, *uv_pointer = NULL;
  int y_pitch = kWidth;
  int uv_pitch = 2 * ((kWidth + 1) >> 1);
  int u_pitch = (kWidth + 1) >> 1;
  int v_pitch = (kWidth + 1) >> 1;
  int y_size = kHeight * kWidth;
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1);
  int block_size = 2;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> yuv_input(
      CreateFakeNV12TestingImage(kHeight, kWidth, block_size,
                                 y_pointer, uv_pointer));
  // Allocate space for the output image.
  rtc::scoped_ptr<uint8[]> yuv_output(
      new uint8[I420_SIZE(kHeight, kWidth) + kAlignment + unalignment]);
  uint8 *y_output_pointer = ALIGNP(yuv_output.get(), kAlignment) + unalignment;
  uint8 *u_output_pointer = y_output_pointer + y_size;
  uint8 *v_output_pointer = u_output_pointer + uv_size;
  // Generate the expected output.
  uint8 *y_expected_pointer = NULL, *u_expected_pointer = NULL,
        *v_expected_pointer = NULL;
  rtc::scoped_ptr<uint8[]> yuv_output_expected(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
          libyuv::kJpegYuv420,
          y_expected_pointer, u_expected_pointer, v_expected_pointer));

  for (int i = 0; i < repeat_; ++i) {
  libyuv::NV12ToI420(y_pointer, y_pitch,
                     uv_pointer, uv_pitch,
                     y_output_pointer, y_pitch,
                     u_output_pointer, u_pitch,
                     v_output_pointer, v_pitch,
                     kWidth, kHeight);
  }
  // Compare the output frame with what is expected; expect exactly the same.
  // Note: MSE should be set to a larger threshold if an odd block width
  // is used, since the conversion will be lossy.
  EXPECT_TRUE(IsMemoryEqual(y_output_pointer, y_expected_pointer,
      I420_SIZE(kHeight, kWidth), 1.e-6));

  if (dump_) { DumpYuvImage(y_output_pointer, kWidth, kHeight); }
}

// A common macro for testing converting YUY2/UYVY to I420.
#define TEST_YUVTOI420(SRC_NAME, MSE, BLOCK_SIZE) \
TEST_P(PlanarFunctionsTest, SRC_NAME##ToI420) { \
  /* Get the unalignment offset.*/ \
  int unalignment = GetParam(); \
  uint8 *yuv_pointer = NULL; \
  int yuv_pitch = 2 * ((kWidth + 1) & ~1); \
  int y_pitch = kWidth; \
  int u_pitch = (kWidth + 1) >> 1; \
  int v_pitch = (kWidth + 1) >> 1; \
  int y_size = kHeight * kWidth; \
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1); \
  int block_size = 2; \
  /* Generate a fake input image.*/ \
  rtc::scoped_ptr<uint8[]> yuv_input( \
      CreateFakeInterleaveYuvTestingImage(kHeight, kWidth, BLOCK_SIZE, \
          yuv_pointer, FOURCC_##SRC_NAME)); \
  /* Allocate space for the output image.*/ \
  rtc::scoped_ptr<uint8[]> yuv_output( \
      new uint8[I420_SIZE(kHeight, kWidth) + kAlignment + unalignment]); \
  uint8 *y_output_pointer = ALIGNP(yuv_output.get(), kAlignment) + \
      unalignment; \
  uint8 *u_output_pointer = y_output_pointer + y_size; \
  uint8 *v_output_pointer = u_output_pointer + uv_size; \
  /* Generate the expected output.*/ \
  uint8 *y_expected_pointer = NULL, *u_expected_pointer = NULL, \
        *v_expected_pointer = NULL; \
  rtc::scoped_ptr<uint8[]> yuv_output_expected( \
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size, \
          libyuv::kJpegYuv420, \
          y_expected_pointer, u_expected_pointer, v_expected_pointer)); \
  for (int i = 0; i < repeat_; ++i) { \
    libyuv::SRC_NAME##ToI420(yuv_pointer, yuv_pitch, \
                             y_output_pointer, y_pitch, \
                             u_output_pointer, u_pitch, \
                             v_output_pointer, v_pitch, \
                             kWidth, kHeight); \
  } \
  /* Compare the output frame with what is expected.*/ \
  /* Note: MSE should be set to a larger threshold if an odd block width*/ \
  /* is used, since the conversion will be lossy.*/ \
  EXPECT_TRUE(IsMemoryEqual(y_output_pointer, y_expected_pointer, \
      I420_SIZE(kHeight, kWidth), MSE)); \
  if (dump_) { DumpYuvImage(y_output_pointer, kWidth, kHeight); } \
} \

// TEST_P(PlanarFunctionsTest, YUV2ToI420)
TEST_YUVTOI420(YUY2, 1.e-6, 2);
// TEST_P(PlanarFunctionsTest, UYVYToI420)
TEST_YUVTOI420(UYVY, 1.e-6, 2);

// A common macro for testing converting I420 to ARGB, BGRA and ABGR.
#define TEST_YUVTORGB(SRC_NAME, DST_NAME, JPG_TYPE, MSE, BLOCK_SIZE) \
TEST_F(PlanarFunctionsTest, SRC_NAME##To##DST_NAME) { \
  uint8 *y_pointer = NULL, *u_pointer = NULL, *v_pointer = NULL; \
  uint8 *argb_expected_pointer = NULL; \
  int y_pitch = kWidth; \
  int u_pitch = (kWidth + 1) >> 1; \
  int v_pitch = (kWidth + 1) >> 1; \
  /* Generate a fake input image.*/ \
  rtc::scoped_ptr<uint8[]> yuv_input( \
      CreateFakeYuvTestingImage(kHeight, kWidth, BLOCK_SIZE, JPG_TYPE, \
                                y_pointer, u_pointer, v_pointer)); \
  /* Generate the expected output.*/ \
  rtc::scoped_ptr<uint8[]> argb_expected( \
      CreateFakeArgbTestingImage(kHeight, kWidth, BLOCK_SIZE, \
                                 argb_expected_pointer, FOURCC_##DST_NAME)); \
  /* Allocate space for the output.*/ \
  rtc::scoped_ptr<uint8[]> argb_output( \
    new uint8[kHeight * kWidth * 4 + kAlignment]); \
  uint8 *argb_pointer = ALIGNP(argb_expected.get(), kAlignment); \
  for (int i = 0; i < repeat_; ++i) { \
    libyuv::SRC_NAME##To##DST_NAME(y_pointer, y_pitch, \
                                   u_pointer, u_pitch, \
                                   v_pointer, v_pitch, \
                                   argb_pointer, \
                                   kWidth * 4, \
                                   kWidth, kHeight); \
  } \
  EXPECT_TRUE(IsMemoryEqual(argb_expected_pointer, argb_pointer, \
                            kHeight * kWidth * 4, MSE)); \
  if (dump_) { DumpArgbImage(argb_pointer, kWidth, kHeight); } \
}

// TEST_F(PlanarFunctionsTest, I420ToARGB)
TEST_YUVTORGB(I420, ARGB, libyuv::kJpegYuv420, 3., 2);
// TEST_F(PlanarFunctionsTest, I420ToABGR)
TEST_YUVTORGB(I420, ABGR, libyuv::kJpegYuv420, 3., 2);
// TEST_F(PlanarFunctionsTest, I420ToBGRA)
TEST_YUVTORGB(I420, BGRA, libyuv::kJpegYuv420, 3., 2);
// TEST_F(PlanarFunctionsTest, I422ToARGB)
TEST_YUVTORGB(I422, ARGB, libyuv::kJpegYuv422, 3., 2);
// TEST_F(PlanarFunctionsTest, I444ToARGB)
TEST_YUVTORGB(I444, ARGB, libyuv::kJpegYuv444, 3., 3);
// Note: an empirical MSE tolerance 3.0 is used here for the probable
// error from float-to-uint8 type conversion.

TEST_F(PlanarFunctionsTest, I400ToARGB_Reference) {
  uint8 *y_pointer = NULL, *u_pointer = NULL, *v_pointer = NULL;
  int y_pitch = kWidth;
  int u_pitch = (kWidth + 1) >> 1;
  int v_pitch = (kWidth + 1) >> 1;
  int block_size = 3;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> yuv_input(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
                                libyuv::kJpegYuv420,
                                y_pointer, u_pointer, v_pointer));
  // As the comparison standard, we convert a grayscale image (by setting both
  // U and V channels to be 128) using an I420 converter.
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1);

  rtc::scoped_ptr<uint8[]> uv(new uint8[uv_size + kAlignment]);
  u_pointer = v_pointer = ALIGNP(uv.get(), kAlignment);
  memset(u_pointer, 128, uv_size);

  // Allocate space for the output image and generate the expected output.
  rtc::scoped_ptr<uint8[]> argb_expected(
      new uint8[kHeight * kWidth * 4 + kAlignment]);
  rtc::scoped_ptr<uint8[]> argb_output(
      new uint8[kHeight * kWidth * 4 + kAlignment]);
  uint8 *argb_expected_pointer = ALIGNP(argb_expected.get(), kAlignment);
  uint8 *argb_pointer = ALIGNP(argb_output.get(), kAlignment);

  libyuv::I420ToARGB(y_pointer, y_pitch,
                     u_pointer, u_pitch,
                     v_pointer, v_pitch,
                     argb_expected_pointer, kWidth * 4,
                     kWidth, kHeight);
  for (int i = 0; i < repeat_; ++i) {
    libyuv::I400ToARGB_Reference(y_pointer, y_pitch,
                                 argb_pointer, kWidth * 4,
                                 kWidth, kHeight);
  }

  // Note: I420ToARGB and I400ToARGB_Reference should produce identical results.
  EXPECT_TRUE(IsMemoryEqual(argb_expected_pointer, argb_pointer,
                            kHeight * kWidth * 4, 2.));
  if (dump_) { DumpArgbImage(argb_pointer, kWidth, kHeight); }
}

TEST_P(PlanarFunctionsTest, I400ToARGB) {
  // Get the unalignment offset
  int unalignment = GetParam();
  uint8 *y_pointer = NULL, *u_pointer = NULL, *v_pointer = NULL;
  int y_pitch = kWidth;
  int u_pitch = (kWidth + 1) >> 1;
  int v_pitch = (kWidth + 1) >> 1;
  int block_size = 3;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> yuv_input(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
                                libyuv::kJpegYuv420,
                                y_pointer, u_pointer, v_pointer));
  // As the comparison standard, we convert a grayscale image (by setting both
  // U and V channels to be 128) using an I420 converter.
  int uv_size = ((kHeight + 1) >> 1) * ((kWidth + 1) >> 1);

  // 1 byte extra if in the unaligned mode.
  rtc::scoped_ptr<uint8[]> uv(new uint8[uv_size * 2 + kAlignment]);
  u_pointer = ALIGNP(uv.get(), kAlignment);
  v_pointer = u_pointer + uv_size;
  memset(u_pointer, 128, uv_size);
  memset(v_pointer, 128, uv_size);

  // Allocate space for the output image and generate the expected output.
  rtc::scoped_ptr<uint8[]> argb_expected(
      new uint8[kHeight * kWidth * 4 + kAlignment]);
  // 1 byte extra if in the misalinged mode.
  rtc::scoped_ptr<uint8[]> argb_output(
      new uint8[kHeight * kWidth * 4 + kAlignment + unalignment]);
  uint8 *argb_expected_pointer = ALIGNP(argb_expected.get(), kAlignment);
  uint8 *argb_pointer = ALIGNP(argb_output.get(), kAlignment) + unalignment;

  libyuv::I420ToARGB(y_pointer, y_pitch,
                     u_pointer, u_pitch,
                     v_pointer, v_pitch,
                     argb_expected_pointer, kWidth * 4,
                     kWidth, kHeight);
  for (int i = 0; i < repeat_; ++i) {
    libyuv::I400ToARGB(y_pointer, y_pitch,
                       argb_pointer, kWidth * 4,
                       kWidth, kHeight);
  }

  // Note: current I400ToARGB uses an approximate method,
  // so the error tolerance is larger here.
  EXPECT_TRUE(IsMemoryEqual(argb_expected_pointer, argb_pointer,
                            kHeight * kWidth * 4, 64.0));
  if (dump_) { DumpArgbImage(argb_pointer, kWidth, kHeight); }
}

TEST_P(PlanarFunctionsTest, ARGBToI400) {
  // Get the unalignment offset
  int unalignment = GetParam();
  // Create a fake ARGB input image.
  uint8 *y_pointer = NULL, *u_pointer = NULL, *v_pointer = NULL;
  uint8 *argb_pointer = NULL;
  int block_size = 3;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8[]> argb_input(
      CreateFakeArgbTestingImage(kHeight, kWidth, block_size,
                                 argb_pointer, FOURCC_ARGB));
  // Generate the expected output. Only Y channel is used
  rtc::scoped_ptr<uint8[]> yuv_expected(
      CreateFakeYuvTestingImage(kHeight, kWidth, block_size,
                                libyuv::kJpegYuv420,
                                y_pointer, u_pointer, v_pointer));
  // Allocate space for the Y output.
  rtc::scoped_ptr<uint8[]> y_output(
    new uint8[kHeight * kWidth + kAlignment + unalignment]);
  uint8 *y_output_pointer = ALIGNP(y_output.get(), kAlignment) + unalignment;

  for (int i = 0; i < repeat_; ++i) {
    libyuv::ARGBToI400(argb_pointer, kWidth * 4, y_output_pointer, kWidth,
                       kWidth, kHeight);
  }
  // Check if the output matches the input Y channel.
  // Note: an empirical MSE tolerance 2.0 is used here for the probable
  // error from float-to-uint8 type conversion.
  EXPECT_TRUE(IsMemoryEqual(y_output_pointer, y_pointer,
                            kHeight * kWidth, 2.));
  if (dump_) { DumpArgbImage(argb_pointer, kWidth, kHeight); }
}

// A common macro for testing converting RAW, BG24, BGRA, and ABGR
// to ARGB.
#define TEST_ARGB(SRC_NAME, FC_ID, BPP, BLOCK_SIZE) \
TEST_P(PlanarFunctionsTest, SRC_NAME##ToARGB) { \
  int unalignment = GetParam();  /* Get the unalignment offset.*/ \
  uint8 *argb_expected_pointer = NULL, *src_pointer = NULL; \
  /* Generate a fake input image.*/ \
  rtc::scoped_ptr<uint8[]> src_input(  \
      CreateFakeArgbTestingImage(kHeight, kWidth, BLOCK_SIZE, \
                                 src_pointer, FOURCC_##FC_ID)); \
  /* Generate the expected output.*/ \
  rtc::scoped_ptr<uint8[]> argb_expected( \
      CreateFakeArgbTestingImage(kHeight, kWidth, BLOCK_SIZE, \
                                 argb_expected_pointer, FOURCC_ARGB)); \
  /* Allocate space for the output; 1 byte extra if in the unaligned mode.*/ \
  rtc::scoped_ptr<uint8[]> argb_output( \
      new uint8[kHeight * kWidth * 4 + kAlignment + unalignment]); \
  uint8 *argb_pointer = ALIGNP(argb_output.get(), kAlignment) + unalignment; \
  for (int i = 0; i < repeat_; ++i) { \
    libyuv:: SRC_NAME##ToARGB(src_pointer, kWidth * (BPP), argb_pointer, \
                              kWidth * 4, kWidth, kHeight); \
  } \
  /* Compare the result; expect identical.*/ \
  EXPECT_TRUE(IsMemoryEqual(argb_expected_pointer, argb_pointer, \
                            kHeight * kWidth * 4, 1.e-6)); \
  if (dump_) { DumpArgbImage(argb_pointer, kWidth, kHeight); } \
}

TEST_ARGB(RAW, RAW, 3, 3);    // TEST_P(PlanarFunctionsTest, RAWToARGB)
TEST_ARGB(BG24, 24BG, 3, 3);  // TEST_P(PlanarFunctionsTest, BG24ToARGB)
TEST_ARGB(ABGR, ABGR, 4, 3);  // TEST_P(PlanarFunctionsTest, ABGRToARGB)
TEST_ARGB(BGRA, BGRA, 4, 3);  // TEST_P(PlanarFunctionsTest, BGRAToARGB)

// Parameter Test: The parameter is the unalignment offset.
// Aligned data for testing assembly versions.
INSTANTIATE_TEST_CASE_P(PlanarFunctionsAligned, PlanarFunctionsTest,
    ::testing::Values(0));

// Purposely unalign the output argb pointer to test slow path (C version).
INSTANTIATE_TEST_CASE_P(PlanarFunctionsMisaligned, PlanarFunctionsTest,
    ::testing::Values(1));

}  // namespace cricket
