
#include "roo_display/driver/common/addr_window_device.h"

#include <memory>
#include <random>

#include "gtest/gtest-param-test.h"
#include "roo_io/data/byte_order.h"
#include "testing_display_device.h"

using namespace testing;

namespace roo_display {

template <typename ColorModeP, ByteOrder byte_order_p>
class TestTarget {
 public:
  typedef ColorModeP ColorMode;
  static constexpr ByteOrder byte_order = byte_order_p;

  TestTarget(int16_t width, int16_t height, Color bg)
      : width_(width),
        height_(height),
        data_(new Color[width * height]),
        orientation_(Orientation::Default()),
        xMin_(-1),
        yMin_(-1),
        xMax_(-1),
        yMax_(-1),
        xCursor_(-1),
        yCursor_(-1),
        initialized_(false),
        inTransaction_(false),
        inRamWrite_(false),
        flushed_(true) {
    ColorMode mode;
    bg = mode.toArgbColor(mode.fromArgbColor(bg));
    std::fill(&data_[0], &data_[width * height], bg);
  }

  int16_t width() const { return width_; }
  int16_t height() const { return height_; }

  void init() { initialized_ = true; }

  void begin() {
    ASSERT_FALSE(inTransaction_);
    inTransaction_ = true;
  }

  void end() {
    ASSERT_TRUE(inTransaction_);
    inTransaction_ = false;
  }

  void setAddrWindow(int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
    xMin_ = x0;
    yMin_ = y0;
    xMax_ = x1;
    yMax_ = y1;
    xCursor_ = xMin_;
    yCursor_ = yMin_;
  }

  void startRamWrite() { inRamWrite_ = true; }

  void setOrientation(Orientation orientation) { orientation_ = orientation; }

  void flush() { flushed_ = true; }

  void ramWrite(const roo::byte* raw_color, size_t count) {
    EXPECT_TRUE(inRamWrite_);
    ColorIo<ColorMode, byte_order> io;
    while (count-- > 0) {
      Color color = io.load(raw_color);
      setPixel(xCursor_, yCursor_, color);
      raw_color += ColorTraits<ColorMode>::bytes_per_pixel;
      xCursor_++;
      if (xCursor_ > xMax_) {
        xCursor_ = xMin_;
        yCursor_++;
      }
    }
  }

  void ramFill(const roo::byte* raw_color, size_t count) {
    EXPECT_TRUE(inRamWrite_);
    Color color = ColorIo<ColorMode, byte_order>().load(raw_color);
    while (count-- > 0) {
      setPixel(xCursor_, yCursor_, color);
      xCursor_++;
      if (xCursor_ > xMax_) {
        xCursor_ = xMin_;
        yCursor_++;
      }
    }
  }

  void ramFillOnce(const roo::byte* raw_color, size_t count) {
    EXPECT_TRUE(flushed_);
    ramFill(raw_color, count);
  }

  const Color* data() const { return data_.get(); }

 private:
  void setPixel(int16_t x, int16_t y, Color color) {
    ASSERT_GE(x, 0);
    ASSERT_GE(y, 0);
    if (orientation_.isXYswapped()) {
      std::swap(x, y);
    }
    ASSERT_LT(x, width());
    ASSERT_LT(y, height());
    if (orientation_.isRightToLeft()) x = width() - x - 1;
    if (orientation_.isBottomToTop()) y = height() - y - 1;
    data_[x + y * width()] = color;
  }

  int16_t width_, height_;
  std::unique_ptr<Color[]> data_;
  Orientation orientation_;

  int16_t xMin_, yMin_, xMax_, yMax_;
  int16_t xCursor_, yCursor_;

  bool initialized_;
  bool inTransaction_;
  bool inRamWrite_;
  bool flushed_;
};

template <typename ColorMode, ByteOrder byte_order>
class TestDevice : public AddrWindowDevice<TestTarget<ColorMode, byte_order>> {
 public:
  typedef AddrWindowDevice<TestTarget<ColorMode, byte_order>> Base;

  TestDevice(int16_t width, int16_t height, Color color)
      : Base(TestTarget<ColorMode, byte_order>(width, height, color)) {
    Base::init();
  }

  const Color* data() const { return Base::target_.data(); }
};

template <typename ColorMode, ByteOrder byte_order>
const TestColorStreamable<ColorMode> RasterOf(
    const TestDevice<ColorMode, byte_order>& device) {
  return TestColorStreamable<ColorMode>(device.raw_width(), device.raw_height(),
                                        device.data());
}

using Rgb565TestDevice = TestDevice<Rgb565, roo_io::kBigEndian>;
using Rgb565RefDevice = FakeOffscreen<Rgb565, roo_io::kBigEndian>;

class AddrWindowDeviceTest
    : public testing::TestWithParam<std::tuple<BlendingMode, Orientation>> {};

class AddrWindowDeviceBlendingTest
    : public testing::TestWithParam<std::tuple<BlendingMode, Orientation>> {};

TEST_P(AddrWindowDeviceTest, FillRects) {
  TestFillRects<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                   std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, FillHLines) {
  TestFillHLines<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, FillVLines) {
  TestFillVLines<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, FillDegeneratePixels) {
  TestFillDegeneratePixels<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, FillPixels) {
  TestFillPixels<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRects) {
  TestWriteRects<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteHLines) {
  TestWriteHLines<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                     std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteVLines) {
  TestWriteVLines<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                     std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteDegeneratePixels) {
  TestWriteDegeneratePixels<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WritePixels) {
  TestWritePixels<Rgb565TestDevice, Rgb565RefDevice>(std::get<0>(GetParam()),
                                                     std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WritePixelsStress) {
  TestWritePixelsStress<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WritePixelsSnake) {
  TestWritePixelsSnake<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRectWindowSimple) {
  TestWriteRectWindowSimple<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRectWindowStress) {
  TestWriteRectWindowStress<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRectWindowGrayscale8) {
  TestWriteRectWindowSimple<TestDevice<Grayscale8, roo_io::kBigEndian>,
                            FakeOffscreen<Grayscale8, roo_io::kBigEndian>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRectWindowSimpleLE) {
  TestWriteRectWindowSimple<TestDevice<Rgb565, roo_io::kLittleEndian>,
                            FakeOffscreen<Rgb565, roo_io::kLittleEndian>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRectWindowSimpleArgb4444LE) {
  TestWriteRectWindowSimple<TestDevice<Argb4444, roo_io::kLittleEndian>,
                            FakeOffscreen<Argb4444, roo_io::kLittleEndian>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, WriteRectWindowSimpleArgb8888LE) {
  TestWriteRectWindowSimple<TestDevice<Argb8888, roo_io::kLittleEndian>,
                            FakeOffscreen<Argb8888, roo_io::kLittleEndian>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceTest, DrawDirectRect) {
  TestDrawDirectRect<Rgb565TestDevice, Rgb565RefDevice>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(AddrWindowDeviceBlendingTest, FillPixelsSparse) {
  Color kBg = Rgb565().toArgbColor(Rgb565().fromArgbColor(Color(0xFF102040)));
  TestDisplayDevice<Rgb565TestDevice, Rgb565RefDevice> screen(32, 24, kBg);
  screen.setOrientation(std::get<1>(GetParam()));
  screen.test().setBgColorHint(kBg);
  int16_t x[] = {1, 8, 15, 20, 23};
  int16_t y[] = {2, 7, 12, 17, 22};
  screen.fillPixels(std::get<0>(GetParam()), Color(0x77445566), x, y, 5);
  EXPECT_CONSISTENT(screen);
}

TEST_P(AddrWindowDeviceBlendingTest, WritePixelsSparse) {
  Color kBg = Rgb565().toArgbColor(Rgb565().fromArgbColor(Color(0xFF305020)));
  TestDisplayDevice<Rgb565TestDevice, Rgb565RefDevice> screen(30, 26, kBg);
  screen.setOrientation(std::get<1>(GetParam()));
  screen.test().setBgColorHint(kBg);
  int16_t x[] = {3, 7, 11, 19, 25};
  int16_t y[] = {4, 9, 14, 19, 24};
  Color c[] = {Color(0x55336699), Color(0xAA114477), Color(0xCC552211),
               Color(0xFF22AA44), Color(0x7F8844CC)};
  screen.writePixels(std::get<0>(GetParam()), c, x, y, 5);
  EXPECT_CONSISTENT(screen);
}

TEST_P(AddrWindowDeviceBlendingTest, AddressWindowWriteSpan) {
  Color kBg = Rgb565().toArgbColor(Rgb565().fromArgbColor(Color(0xFF223344)));
  TestDisplayDevice<Rgb565TestDevice, Rgb565RefDevice> screen(20, 18, kBg);
  screen.setOrientation(std::get<1>(GetParam()));
  screen.test().setBgColorHint(kBg);
  Color c[] = {Color(0x88224488), Color(0xCCAA3300), Color(0x77AAEE22),
               Color(0xFF1166BB), Color(0x33445566)};
  screen.setAddress(4, 11, 8, 11, std::get<0>(GetParam()));
  screen.write(c, 5);
  EXPECT_CONSISTENT(screen);
}

TEST_P(AddrWindowDeviceBlendingTest, AddressWindowFillSpan) {
  Color kBg = Rgb565().toArgbColor(Rgb565().fromArgbColor(Color(0xFF405060)));
  TestDisplayDevice<Rgb565TestDevice, Rgb565RefDevice> screen(22, 20, kBg);
  screen.setOrientation(std::get<1>(GetParam()));
  screen.test().setBgColorHint(kBg);
  screen.setAddress(6, 3, 10, 3, std::get<0>(GetParam()));
  screen.fill(Color(0x9966AA44), 5);
  EXPECT_CONSISTENT(screen);
}

INSTANTIATE_TEST_SUITE_P(
    AddrWindowDeviceTests, AddrWindowDeviceTest,
    testing::Combine(
        testing::Values(BlendingMode::kSource),
        testing::Values(Orientation::RightDown(), Orientation::DownRight(),
                        Orientation::LeftDown(), Orientation::DownLeft(),
                        Orientation::RightUp(), Orientation::UpRight(),
                        Orientation::LeftUp(), Orientation::UpLeft())));

INSTANTIATE_TEST_SUITE_P(
    AddrWindowDeviceTestsBlending, AddrWindowDeviceBlendingTest,
    testing::Combine(
        testing::Values(BlendingMode::kSource, BlendingMode::kSourceOver,
                        BlendingMode::kSourceOverOpaque,
                        BlendingMode::kSourceIn, BlendingMode::kSourceOut,
                        BlendingMode::kSourceAtop, BlendingMode::kDestination,
                        BlendingMode::kDestinationOver,
                        BlendingMode::kDestinationIn,
                        BlendingMode::kDestinationOut,
                        BlendingMode::kDestinationAtop, BlendingMode::kXor,
                        BlendingMode::kClear),
        testing::Values(Orientation::RightDown(), Orientation::DownLeft())));

}  // namespace roo_display
