/*
 * 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_t[kTestingColorNum]);
    testing_color_u_.reset(new uint8_t[kTestingColorNum]);
    testing_color_v_.reset(new uint8_t[kTestingColorNum]);
    testing_color_r_.reset(new uint8_t[kTestingColorNum]);
    testing_color_g_.reset(new uint8_t[kTestingColorNum]);
    testing_color_b_.reset(new uint8_t[kTestingColorNum]);
    int color_counter = 0;
    for (int i = 0; i < kTestingColorChannelResolution; ++i) {
      uint8_t color_r =
          static_cast<uint8_t>(i * 255 / (kTestingColorChannelResolution - 1));
      for (int j = 0; j < kTestingColorChannelResolution; ++j) {
        uint8_t color_g = static_cast<uint8_t>(
            j * 255 / (kTestingColorChannelResolution - 1));
        for (int k = 0; k < kTestingColorChannelResolution; ++k) {
          uint8_t color_b = static_cast<uint8_t>(
              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_t r,
                       uint8_t g,
                       uint8_t b,
                       uint8_t* y,
                       uint8_t* u,
                       uint8_t* 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_t ClampUint8(double value) {
    value = std::max(0., std::min(255., value));
    uint8_t uint8_value = static_cast<uint8_t>(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_t* CreateFakeYuvTestingImage(int height,
                                     int width,
                                     int block_size,
                                     libyuv::JpegSubsamplingType subsample_type,
                                     uint8_t*& y_pointer,
                                     uint8_t*& u_pointer,
                                     uint8_t*& 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_t* image_pointer = new uint8_t[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_t* current_y_pointer = y_pointer;
    uint8_t* current_u_pointer = u_pointer;
    uint8_t* 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_t* CreateFakeInterleaveYuvTestingImage(int height,
                                               int width,
                                               int block_size,
                                               uint8_t*& 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_t* image_pointer = new uint8_t[2 * height * awidth + kAlignment];
    yuv_pointer = ALIGNP(image_pointer, kAlignment);
    uint8_t* 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_t>(
                  (static_cast<uint32_t>(testing_color_u_[color1]) +
                   static_cast<uint32_t>(testing_color_u_[color2])) /
                  2);
              current_yuv_pointer[2] = testing_color_y_[color2];
              current_yuv_pointer[3] = static_cast<uint8_t>(
                  (static_cast<uint32_t>(testing_color_v_[color1]) +
                   static_cast<uint32_t>(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_t>(
                  (static_cast<uint32_t>(testing_color_u_[color1]) +
                   static_cast<uint32_t>(testing_color_u_[color2])) /
                  2);
              current_yuv_pointer[1] = testing_color_y_[color1];
              current_yuv_pointer[2] = static_cast<uint8_t>(
                  (static_cast<uint32_t>(testing_color_v_[color1]) +
                   static_cast<uint32_t>(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_t* CreateFakeNV12TestingImage(int height,
                                      int width,
                                      int block_size,
                                      uint8_t*& y_pointer,
                                      uint8_t*& uv_pointer) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }

    uint8_t* image_pointer =
        new uint8_t[height * width +
                    ((height + 1) / 2) * ((width + 1) / 2) * 2 + kAlignment];
    y_pointer = ALIGNP(image_pointer, kAlignment);
    uv_pointer = y_pointer + height * width;
    uint8_t* current_uv_pointer = uv_pointer;
    uint8_t* 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_t* CreateFakeM420TestingImage(int height,
                                      int width,
                                      int block_size,
                                      uint8_t*& m420_pointer) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }

    uint8_t* image_pointer =
        new uint8_t[height * width +
                    ((height + 1) / 2) * ((width + 1) / 2) * 2 + kAlignment];
    m420_pointer = ALIGNP(image_pointer, kAlignment);
    uint8_t* 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_t* CreateFakeArgbTestingImage(int height,
                                      int width,
                                      int block_size,
                                      uint8_t*& argb_pointer,
                                      FourCC fourcc_type) {
    if (height <= 0 || width <= 0 || block_size <= 0) { return NULL; }
    uint8_t* image_pointer = NULL;
    if (fourcc_type == FOURCC_ABGR || fourcc_type == FOURCC_BGRA ||
        fourcc_type == FOURCC_ARGB) {
      image_pointer = new uint8_t[height * width * 4 + kAlignment];
    } else if (fourcc_type == FOURCC_RAW || fourcc_type == FOURCC_24BG) {
      image_pointer = new uint8_t[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_t* 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_t* ibuf,
                            const uint8_t* 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_t* buf1, const uint8_t* 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_t* obuf, int width, int height) {
    DumpPlanarArgbTestImage(GetTestName(), obuf, width, height);
  }

  // Dump the result image (YUV420 format).
  void DumpYuvImage(const uint8_t* 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_t[]> testing_color_y_;
  rtc::scoped_ptr<uint8_t[]> testing_color_u_;
  rtc::scoped_ptr<uint8_t[]> testing_color_v_;
  rtc::scoped_ptr<uint8_t[]> testing_color_r_;
  rtc::scoped_ptr<uint8_t[]> testing_color_g_;
  rtc::scoped_ptr<uint8_t[]> testing_color_b_;
};

TEST_F(PlanarFunctionsTest, I420Copy) {
  uint8_t* y_pointer = nullptr;
  uint8_t* u_pointer = nullptr;
  uint8_t* v_pointer = nullptr;
  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_t[]> 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_t[]> yuv_output(
      new uint8_t[I420_SIZE(kHeight, kWidth) + kAlignment]);
  uint8_t* y_output_pointer = ALIGNP(yuv_output.get(), kAlignment);
  uint8_t* u_output_pointer = y_output_pointer + y_size;
  uint8_t* 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_t* y_pointer = nullptr;
  uint8_t* u_pointer = nullptr;
  uint8_t* v_pointer = nullptr;
  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_t[]> 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_t[]> yuv_output(
      new uint8_t[I420_SIZE(kHeight, kWidth) + kAlignment]);
  uint8_t* y_output_pointer = ALIGNP(yuv_output.get(), kAlignment);
  uint8_t* u_output_pointer = y_output_pointer + y_size;
  uint8_t* v_output_pointer = u_output_pointer + uv_size;
  // Generate the expected output.
  uint8_t* y_expected_pointer = nullptr;
  uint8_t* u_expected_pointer = nullptr;
  uint8_t* v_expected_pointer = nullptr;
  rtc::scoped_ptr<uint8_t[]> 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_t* 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_t[]> yuv_input(
      CreateFakeM420TestingImage(kHeight, kWidth, block_size, m420_pointer));
  // Allocate space for the output image.
  rtc::scoped_ptr<uint8_t[]> yuv_output(
      new uint8_t[I420_SIZE(kHeight, kWidth) + kAlignment + unalignment]);
  uint8_t* y_output_pointer =
      ALIGNP(yuv_output.get(), kAlignment) + unalignment;
  uint8_t* u_output_pointer = y_output_pointer + y_size;
  uint8_t* v_output_pointer = u_output_pointer + uv_size;
  // Generate the expected output.
  uint8_t* y_expected_pointer = nullptr;
  uint8_t* u_expected_pointer = nullptr;
  uint8_t* v_expected_pointer = nullptr;
  rtc::scoped_ptr<uint8_t[]> 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_t* y_pointer = nullptr;
  uint8_t* uv_pointer = nullptr;
  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_t[]> yuv_input(CreateFakeNV12TestingImage(
      kHeight, kWidth, block_size, y_pointer, uv_pointer));
  // Allocate space for the output image.
  rtc::scoped_ptr<uint8_t[]> yuv_output(
      new uint8_t[I420_SIZE(kHeight, kWidth) + kAlignment + unalignment]);
  uint8_t* y_output_pointer =
      ALIGNP(yuv_output.get(), kAlignment) + unalignment;
  uint8_t* u_output_pointer = y_output_pointer + y_size;
  uint8_t* v_output_pointer = u_output_pointer + uv_size;
  // Generate the expected output.
  uint8_t* y_expected_pointer = nullptr;
  uint8_t* u_expected_pointer = nullptr;
  uint8_t* v_expected_pointer = nullptr;
  rtc::scoped_ptr<uint8_t[]> 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_t* yuv_pointer = nullptr;                                           \
    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_t[]> yuv_input(CreateFakeInterleaveYuvTestingImage( \
        kHeight, kWidth, BLOCK_SIZE, yuv_pointer, FOURCC_##SRC_NAME));        \
    /* Allocate space for the output image.*/                                 \
    rtc::scoped_ptr<uint8_t[]> yuv_output(                                    \
        new uint8_t[I420_SIZE(kHeight, kWidth) + kAlignment + unalignment]);  \
    uint8_t* y_output_pointer =                                               \
        ALIGNP(yuv_output.get(), kAlignment) + unalignment;                   \
    uint8_t* u_output_pointer = y_output_pointer + y_size;                    \
    uint8_t* v_output_pointer = u_output_pointer + uv_size;                   \
    /* Generate the expected output.*/                                        \
    uint8_t* y_expected_pointer = nullptr;                                    \
    uint8_t* u_expected_pointer = nullptr;                                    \
    uint8_t* v_expected_pointer = nullptr;                                    \
    rtc::scoped_ptr<uint8_t[]> 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_t* y_pointer = nullptr;                                              \
    uint8_t* u_pointer = nullptr;                                              \
    uint8_t* v_pointer = nullptr;                                              \
    uint8_t* 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_t[]> yuv_input(                                      \
        CreateFakeYuvTestingImage(kHeight, kWidth, BLOCK_SIZE, JPG_TYPE,       \
                                  y_pointer, u_pointer, v_pointer));           \
    /* Generate the expected output.*/                                         \
    rtc::scoped_ptr<uint8_t[]> argb_expected(                                  \
        CreateFakeArgbTestingImage(kHeight, kWidth, BLOCK_SIZE,                \
                                   argb_expected_pointer, FOURCC_##DST_NAME)); \
    /* Allocate space for the output.*/                                        \
    rtc::scoped_ptr<uint8_t[]> argb_output(                                    \
        new uint8_t[kHeight * kWidth * 4 + kAlignment]);                       \
    uint8_t* 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_t type conversion.

TEST_F(PlanarFunctionsTest, I400ToARGB_Reference) {
  uint8_t* y_pointer = nullptr;
  uint8_t* u_pointer = nullptr;
  uint8_t* v_pointer = nullptr;
  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_t[]> 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_t[]> uv(new uint8_t[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_t[]> argb_expected(
      new uint8_t[kHeight * kWidth * 4 + kAlignment]);
  rtc::scoped_ptr<uint8_t[]> argb_output(
      new uint8_t[kHeight * kWidth * 4 + kAlignment]);
  uint8_t* argb_expected_pointer = ALIGNP(argb_expected.get(), kAlignment);
  uint8_t* 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_t* y_pointer = nullptr;
  uint8_t* u_pointer = nullptr;
  uint8_t* v_pointer = nullptr;
  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_t[]> 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_t[]> uv(new uint8_t[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_t[]> argb_expected(
      new uint8_t[kHeight * kWidth * 4 + kAlignment]);
  // 1 byte extra if in the misalinged mode.
  rtc::scoped_ptr<uint8_t[]> argb_output(
      new uint8_t[kHeight * kWidth * 4 + kAlignment + unalignment]);
  uint8_t* argb_expected_pointer = ALIGNP(argb_expected.get(), kAlignment);
  uint8_t* 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_t* y_pointer = NULL, * u_pointer = NULL, * v_pointer = NULL;
  uint8_t* argb_pointer = NULL;
  int block_size = 3;
  // Generate a fake input image.
  rtc::scoped_ptr<uint8_t[]> argb_input(CreateFakeArgbTestingImage(
      kHeight, kWidth, block_size, argb_pointer, FOURCC_ARGB));
  // Generate the expected output. Only Y channel is used
  rtc::scoped_ptr<uint8_t[]> 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_t[]> y_output(
      new uint8_t[kHeight * kWidth + kAlignment + unalignment]);
  uint8_t* 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_t 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_t* argb_expected_pointer = NULL, * src_pointer = NULL;               \
    /* Generate a fake input image.*/                                          \
    rtc::scoped_ptr<uint8_t[]> src_input(CreateFakeArgbTestingImage(           \
        kHeight, kWidth, BLOCK_SIZE, src_pointer, FOURCC_##FC_ID));            \
    /* Generate the expected output.*/                                         \
    rtc::scoped_ptr<uint8_t[]> 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_t[]> argb_output(                                    \
        new uint8_t[kHeight * kWidth * 4 + kAlignment + unalignment]);         \
    uint8_t* 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
