
#include "roo_display/core/offscreen.h"

#include <limits>
#include <memory>
#include <random>
#include <utility>

#include "gtest/gtest-param-test.h"
#include "roo_display.h"
#include "roo_display/color/color.h"
#include "roo_display/color/color_mode_indexed.h"
#include "roo_display/core/streamable.h"
#include "roo_display/font/font.h"
#include "roo_display/internal/color_io.h"
#include "roo_fonts/NotoSerif_Italic/12.h"
#include "testing_display_device.h"

using namespace testing;

// static std::default_random_engine generator;

namespace roo_display {

namespace {

void FillIndexedPalette(Color* colors, int size) {
  colors[0] = color::Transparent;
  for (int i = 1; i < size; ++i) {
    colors[i] = Color((i * 37) & 0xFF, (i * 59) & 0xFF, (i * 83) & 0xFF);
  }
}

Palette& IndexedPalette1() {
  static Color colors[2];
  static Palette palette = []() {
    FillIndexedPalette(colors, 2);
    return Palette::ReadWrite(colors, 2);
  }();
  return palette;
}

Palette& IndexedPalette2() {
  static Color colors[4];
  static Palette palette = []() {
    FillIndexedPalette(colors, 4);
    return Palette::ReadWrite(colors, 4);
  }();
  return palette;
}

Palette& IndexedPalette4() {
  static Color colors[16];
  static Palette palette = []() {
    FillIndexedPalette(colors, 16);
    return Palette::ReadWrite(colors, 16);
  }();
  return palette;
}

Palette& IndexedPalette8() {
  static Color colors[256];
  static Palette palette = []() {
    FillIndexedPalette(colors, 256);
    return Palette::ReadWrite(colors, 256);
  }();
  return palette;
}

}  // namespace

// Trivial, byte-order-respecting, raw color reader, returning storage_type.
template <ByteOrder byte_order>
uint32_t read_bytes(const roo::byte* p, int count);

template <>
uint32_t read_bytes<roo_io::kBigEndian>(const roo::byte* p, int count) {
  uint32_t result = 0;
  while (count-- > 0) {
    result <<= 8;
    result |= (uint32_t)*p++;
  }
  return result;
}

template <>
uint32_t read_bytes<roo_io::kLittleEndian>(const roo::byte* p, int count) {
  uint32_t result = 0;
  p += count - 1;
  while (count-- > 0) {
    result <<= 8;
    result |= (uint32_t)*p--;
  }
  return result;
}

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          typename storage_type = ColorStorageType<ColorMode>,
          int bytes_per_pixel = ColorTraits<ColorMode>::bytes_per_pixel,
          int pixels_per_byte = ColorTraits<ColorMode>::pixels_per_byte,
          bool sub_pixel = (ColorTraits<ColorMode>::pixels_per_byte > 1)>
class RawColorReader {
 public:
  RawColorReader(const roo::byte* data, Box extents, ColorMode color_mode)
      : color_mode_(std::move(color_mode)), data_(data), extents_(extents) {}

  Color get(int16_t x, int16_t y) const {
    int size = ColorMode::bits_per_pixel / 8;
    uint32_t offset =
        (x - extents_.xMin() + (y - extents_.yMin()) * extents_.width());
    const roo::byte* ptr = data_ + (offset * size);
    return color_mode_.toArgbColor(read_bytes<byte_order>(ptr, size));
  }

 private:
  ColorMode color_mode_;
  const roo::byte* data_;
  Box extents_;
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          int pixels_per_byte>
class RawColorReader<ColorMode, pixel_order, byte_order, uint8_t, 1,
                     pixels_per_byte, true> {
 public:
  RawColorReader(const roo::byte* data, Box extents, ColorMode color_mode,
                 int pixel_index = 0)
      : color_mode_(std::move(color_mode)),
        data_(data),
        extents_(extents),
        pixel_index_(pixel_index) {}

  Color get(int16_t x, int16_t y) const {
    uint32_t offset =
        (x - extents_.xMin() + (y - extents_.yMin()) * extents_.width());
    SubByteColorIo<ColorMode, pixel_order> helper;
    int32_t byte_offset = (offset + pixel_index_) / pixels_per_byte;
    int pixel_idx = (offset + pixel_index_) % pixels_per_byte;
    return color_mode_.toArgbColor(
        helper.loadRaw(data_[byte_offset], pixel_idx));
  }

 private:
  ColorMode color_mode_;
  const roo::byte* data_;
  Box extents_;
  int pixel_index_;
};

class TrivialColorReader {
 public:
  template <typename ColorMode>
  TrivialColorReader(const Color* colors, Box extents, ColorMode mode)
      : colors_(colors), extents_(extents) {}

  Color get(int16_t x, int16_t y) const {
    uint32_t offset =
        (x - extents_.xMin() + (y - extents_.yMin()) * extents_.width());
    return colors_[offset];
  }

 private:
  const Color* colors_;
  Box extents_;
};

template <typename Reader>
class ColorStream : public PixelStream {
 public:
  using PixelStream::read;

  template <typename... Args>
  ColorStream(Box bounds, Args&&... args)
      : reader_(std::forward<Args>(args)...),
        bounds_(std::move(bounds)),
        x_(bounds_.xMin()),
        y_(bounds_.yMin()) {}

  void read(Color* buf, uint16_t size, uint32_t& run_length) override {
    run_length = 0;
    for (int i = 0; i < size; ++i) {
      buf[i++] = next();
    }
  }

  void skip(uint32_t count) override {
    auto w = bounds_.width();
    y_ += count / w;
    x_ += count % w;
    if (x_ > bounds_.xMax()) {
      x_ -= w;
      ++y_;
    }
  }

  Color next() {
    Color result = reader_.get(x_, y_);
    if (x_ < bounds_.xMax()) {
      ++x_;
    } else {
      x_ = bounds_.xMin();
      ++y_;
    }
    return result;
  }

 private:
  Reader reader_;
  Box bounds_;
  int16_t x_, y_;
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order>
class RawColorRect : public Streamable {
 public:
  typedef RawColorReader<ColorMode, pixel_order, byte_order> Reader;

  RawColorRect(int16_t width, int16_t height, const roo::byte* data,
               const ColorMode& color_mode = ColorMode())
      : extents_(Box(0, 0, width - 1, height - 1)),
        data_(data),
        color_mode_(color_mode) {}

  std::unique_ptr<PixelStream> createStream() const override {
    return std::unique_ptr<PixelStream>(
        new ColorStream<Reader>(extents_, data_, extents_, color_mode_));
  }

  std::unique_ptr<PixelStream> createStream(const Box& bounds) const override {
    return std::unique_ptr<PixelStream>(
        new ColorStream<Reader>(bounds, data_, extents_, color_mode_));
  }

  std::unique_ptr<ColorStream<Reader>> createRawStream() const {
    return std::unique_ptr<ColorStream<Reader>>(
        new ColorStream<Reader>(extents_, data_, extents_, color_mode_));
  }

  Box extents() const override { return extents_; }

  const ColorMode& color_mode() const { return color_mode_; }

 private:
  Box extents_;
  const roo::byte* data_;
  ColorMode color_mode_;
};

template <typename ColorMode>
class TrivialColorRect : public Streamable {
 public:
  TrivialColorRect(int16_t width, int16_t height, const Color* colors,
                   const ColorMode& color_mode = ColorMode())
      : extents_(Box(0, 0, width - 1, height - 1)),
        colors_(colors),
        color_mode_(color_mode) {}

  std::unique_ptr<PixelStream> createStream() const override {
    return std::unique_ptr<PixelStream>(new ColorStream<TrivialColorReader>(
        extents_, colors_, extents_, color_mode_));
  }

  std::unique_ptr<PixelStream> createStream(const Box& bounds) const override {
    return std::unique_ptr<PixelStream>(new ColorStream<TrivialColorReader>(
        bounds, colors_, extents_, color_mode_));
  }

  std::unique_ptr<ColorStream<TrivialColorReader>> createRawStream() const {
    return std::unique_ptr<ColorStream<TrivialColorReader>>(
        new ColorStream<TrivialColorReader>(extents_, colors_, extents_,
                                            color_mode_));
  }

  Box extents() const override { return extents_; }

  const ColorMode& color_mode() const { return color_mode_; }

 private:
  Box extents_;
  const Color* colors_;
  ColorMode color_mode_;
};

template <typename ColorMode, BlendingMode blending_mode>
class TrivialWriter {
 public:
  TrivialWriter(const ColorMode& color_mode, const Color* color)
      : color_mode_(color_mode), color_(color) {}

  void operator()(Color* p, uint32_t offset) { operator()(p, offset, 1); }

  void operator()(Color* p, uint32_t offset, uint32_t count) {
    while (count-- > 0) {
      Color bg = p[offset];
      p[offset++] = color_mode_.toArgbColor(color_mode_.fromArgbColor(
          BlendOp<blending_mode>().blend(bg, *color_++)));
    }
  }

 private:
  ColorMode color_mode_;
  const Color* color_;
};

template <typename ColorMode, BlendingMode blending_mode>
class TrivialFiller {
 public:
  TrivialFiller(const ColorMode& color_mode, Color color)
      : color_mode_(color_mode), color_(color) {}

  void operator()(Color* p, uint32_t offset) { operator()(p, offset, 1); }

  void operator()(Color* p, uint32_t offset, uint32_t count) {
    while (count-- > 0) {
      Color bg = p[offset];
      p[offset++] = color_mode_.toArgbColor(color_mode_.fromArgbColor(
          BlendOp<blending_mode>().blend(bg, color_)));
    }
  }

 private:
  ColorMode color_mode_;
  Color color_;
};

// ReplaceWriter

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
class WriterTester {
 public:
  WriterTester(int16_t width, int16_t height, ColorMode color_mode)
      : color_mode_(std::move(color_mode)),
        width_(width),
        height_(height),
        actual_(new roo::byte[(width * height * ColorMode::bits_per_pixel + 7) /
                              8]),
        expected_(new Color[width * height]) {
    Color bg = color_mode_.toArgbColor(0);
    for (int32_t i = 0;
         i < (width * height * ColorMode::bits_per_pixel + 7) / 8; ++i)
      actual_[i] = roo::byte{0};
    for (int32_t i = 0; i < width * height; ++i) expected_[i] = bg;
  }

  void write(Color* colors, uint32_t offset) {
    typename internal::BlendingWriter<
        ColorMode, pixel_order, byte_order>::template Operator<blending_mode>
        tested_(color_mode_, colors);
    TrivialWriter<ColorMode, blending_mode> tester_(color_mode_, colors);
    tested_(actual_.get(), offset);
    tester_(expected_.get(), offset);
    CheckEq();
  }

  void write(Color* colors, uint32_t offset, uint32_t count) {
    typename internal::BlendingWriter<
        ColorMode, pixel_order, byte_order>::template Operator<blending_mode>
        tested_(color_mode_, colors);
    TrivialWriter<ColorMode, blending_mode> tester_(color_mode_, colors);
    tested_(actual_.get(), offset, count);
    tester_(expected_.get(), offset, count);
    CheckEq();
  }

  void fill(Color color, uint32_t offset) {
    typename internal::BlendingFiller<
        ColorMode, pixel_order, byte_order>::template Operator<blending_mode>
        tested_(color_mode_, color);
    TrivialFiller<ColorMode, blending_mode> tester_(color_mode_, color);
    tested_(actual_.get(), offset);
    tester_(expected_.get(), offset);
    CheckEq();
  }

  void fill(Color color, uint32_t offset, uint32_t count) {
    typename internal::BlendingFiller<
        ColorMode, pixel_order, byte_order>::template Operator<blending_mode>
        tested_(color_mode_, color);
    TrivialFiller<ColorMode, blending_mode> tester_(color_mode_, color);
    tested_(actual_.get(), offset, count);
    tester_(expected_.get(), offset, count);
    CheckEq();
  }

 private:
  void CheckEq() const {
    RawColorRect<ColorMode, pixel_order, byte_order> actual(
        width_, height_, actual_.get(), color_mode_);
    TrivialColorRect<ColorMode> expected(width_, height_, expected_.get(),
                                         color_mode_);
    EXPECT_THAT(actual, MatchesContent(expected));
  }

  ColorMode color_mode_;
  int16_t width_;
  int16_t height_;
  std::unique_ptr<roo::byte[]> actual_;
  std::unique_ptr<Color[]> expected_;
};

template <typename ColorMode>
class ColorRandomizer {
 public:
  ColorRandomizer(int size, ColorMode color_mode = ColorMode())
      : color_mode_(std::move(color_mode)),
        distribution_(0, MaxRawValue()),
        colors_(new Color[size]) {
    for (int i = 0; i < size; ++i) {
      colors_[i] = next();
    }
  }

  Color* get() { return colors_.get(); }

 private:
  static ColorStorageType<ColorMode> MaxRawValue() {
    if (ColorMode::bits_per_pixel <= 8) {
      return static_cast<ColorStorageType<ColorMode>>(
          (1u << ColorMode::bits_per_pixel) - 1);
    }
    return std::numeric_limits<ColorStorageType<ColorMode>>::max();
  }

  // Randomly choses a color from the target's color mode space.
  Color next() { return color_mode_.toArgbColor(distribution_(generator)); }

  ColorMode color_mode_;
  std::uniform_int_distribution<ColorStorageType<ColorMode>> distribution_;
  std::unique_ptr<Color[]> colors_;
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
void TestWriteBasic(ColorMode color_mode = ColorMode()) {
  ColorRandomizer<ColorMode> colors(32, color_mode);
  WriterTester<ColorMode, pixel_order, byte_order, blending_mode> tester(
      4, 8, color_mode);
  tester.write(colors.get() + 0, 0);
  tester.write(colors.get() + 1, 3);
  tester.write(colors.get() + 2, 11);
  tester.write(colors.get() + 3, 12);
  tester.write(colors.get() + 4, 13);

  tester.write(colors.get() + 5, 1, 0);
  tester.write(colors.get() + 6, 3, 1);
  tester.write(colors.get() + 7, 6, 1);
  tester.write(colors.get() + 8, 9, 2);
  tester.write(colors.get() + 10, 12, 2);
  tester.write(colors.get() + 12, 15, 3);
  tester.write(colors.get() + 15, 20, 3);

  // Test some overwrites.
  tester.write(colors.get() + 18, 12);
  tester.write(colors.get() + 19, 13);

  tester.write(colors.get() + 20, 6, 1);
  tester.write(colors.get() + 21, 9, 2);
  tester.write(colors.get() + 23, 12, 2);
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
void TestWriteLongRandom(ColorMode color_mode = ColorMode()) {
  ColorRandomizer<ColorMode> colors(32768, color_mode);
  WriterTester<ColorMode, pixel_order, byte_order, blending_mode> tester(
      16, 64, color_mode);
  int pixels_total = 0;
  while (pixels_total < 32768 - 512) {
    uint32_t offset = rand() % 1024;
    uint32_t len = rand() % 512;
    if (offset + len >= 1024) len = 1024 - offset;
    tester.write(colors.get() + pixels_total, offset, len);
    pixels_total += len;
  }
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
void TestFillBasic(ColorMode color_mode) {
  ColorRandomizer<ColorMode> colors(32, color_mode);
  WriterTester<ColorMode, pixel_order, byte_order, blending_mode> tester(
      4, 8, color_mode);
  const Color* c = colors.get();
  tester.fill(c[0], 0);
  tester.fill(c[1], 3);
  tester.fill(c[2], 11);
  tester.fill(c[3], 12);
  tester.fill(c[4], 13);

  tester.fill(c[5], 1, 0);
  tester.fill(c[6], 3, 1);
  tester.fill(c[7], 6, 1);
  tester.fill(c[8], 9, 2);
  tester.fill(c[9], 12, 2);
  tester.fill(c[10], 15, 3);
  tester.fill(c[11], 20, 3);

  // Test some overwrites.
  tester.fill(c[12], 12);
  tester.fill(c[13], 13);

  tester.fill(c[14], 6, 1);
  tester.fill(c[15], 9, 2);
  tester.fill(c[16], 12, 2);
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
void TestFillLongRandom(ColorMode color_mode) {
  ColorRandomizer<ColorMode> colors(128, color_mode);
  WriterTester<ColorMode, pixel_order, byte_order, blending_mode> tester(
      16, 64, color_mode);
  for (int i = 0; i < 128; ++i) {
    uint32_t offset = rand() % 1024;
    uint32_t len = rand() % 512;
    if (offset + len >= 1024) len = 1024 - offset;
    tester.fill(colors.get()[i], offset, len);
  }
};

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
void TestWriter(ColorMode color_mode) {
  TestWriteBasic<ColorMode, pixel_order, byte_order, blending_mode>(color_mode);
  TestWriteLongRandom<ColorMode, pixel_order, byte_order, blending_mode>(
      color_mode);
}

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order>
void TestWriter(ColorMode color_mode) {
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kSource>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kSourceOver>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order,
             BlendingMode::kSourceOverOpaque>(color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kSourceIn>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kSourceOut>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kSourceAtop>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kDestination>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order,
             BlendingMode::kDestinationOver>(color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kDestinationIn>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kDestinationOut>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kXor>(
      color_mode);
  TestWriter<ColorMode, pixel_order, byte_order, BlendingMode::kClear>(
      color_mode);
}

template <typename ColorMode, ColorPixelOrder pixel_order>
void TestWriter(ColorMode color_mode) {
  TestWriter<ColorMode, pixel_order, roo_io::kBigEndian>(color_mode);
  TestWriter<ColorMode, pixel_order, roo_io::kLittleEndian>(color_mode);
}

template <typename ColorMode>
void TestWriter(ColorMode color_mode = ColorMode()) {
  TestWriter<ColorMode, ColorPixelOrder::kLsbFirst>(color_mode);
  TestWriter<ColorMode, ColorPixelOrder::kLsbFirst>(color_mode);
}

TEST(Writer, Grayscale4) { TestWriter<Grayscale4>(); }
TEST(Writer, Grayscale8) { TestWriter<Grayscale8>(); }
TEST(Writer, Argb8888) { TestWriter<Argb8888>(); }
TEST(Writer, Argb6666) { TestWriter<Argb6666>(); }
TEST(Writer, Argb4444) { TestWriter<Argb4444>(); }
TEST(Writer, Rgb565) { TestWriter<Rgb565>(); }
TEST(Writer, Alpha8) { TestWriter<Alpha8>(color::Black); }
TEST(Writer, Alpha4) { TestWriter<Alpha4>(color::Black); }
TEST(Writer, Indexed1) { TestWriter<Indexed1>(Indexed1(&IndexedPalette1())); }
TEST(Writer, Indexed2) { TestWriter<Indexed2>(Indexed2(&IndexedPalette2())); }
TEST(Writer, Indexed4) { TestWriter<Indexed4>(Indexed4(&IndexedPalette4())); }
TEST(Writer, Indexed8) { TestWriter<Indexed8>(Indexed8(&IndexedPalette8())); }

TEST(Writer, Rgb565WithTransparency) {
  TestWriter<Rgb565WithTransparency>(Rgb565WithTransparency(12));
}

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order,
          BlendingMode blending_mode>
void TestFiller(ColorMode color_mode) {
  TestFillBasic<ColorMode, pixel_order, byte_order, blending_mode>(color_mode);
  TestFillLongRandom<ColorMode, pixel_order, byte_order, blending_mode>(
      color_mode);
}

template <typename ColorMode, ColorPixelOrder pixel_order, ByteOrder byte_order>
void TestFiller(ColorMode color_mode) {
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kSource>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kSourceOver>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order,
             BlendingMode::kSourceOverOpaque>(color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kSourceIn>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kSourceOut>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kSourceAtop>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kDestination>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order,
             BlendingMode::kDestinationOver>(color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kDestinationIn>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kDestinationOut>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kXor>(
      color_mode);
  TestFiller<ColorMode, pixel_order, byte_order, BlendingMode::kClear>(
      color_mode);
}

template <typename ColorMode, ColorPixelOrder pixel_order>
void TestFiller(ColorMode color_mode = ColorMode()) {
  TestFiller<ColorMode, pixel_order, roo_io::kBigEndian>(color_mode);
  TestFiller<ColorMode, pixel_order, roo_io::kLittleEndian>(color_mode);
}

template <typename ColorMode>
void TestFiller(ColorMode color_mode = ColorMode()) {
  TestFiller<ColorMode, ColorPixelOrder::kLsbFirst>(color_mode);
  TestFiller<ColorMode, ColorPixelOrder::kLsbFirst>(color_mode);
}

TEST(Filler, Grayscale4) { TestFiller<Grayscale4>(); }
TEST(Filler, Grayscale8) { TestFiller<Grayscale8>(); }
TEST(Filler, Argb8888) { TestFiller<Argb8888>(); }
TEST(Filler, Argb6666) { TestFiller<Argb6666>(); }
TEST(Filler, Argb4444) { TestFiller<Argb4444>(); }
TEST(Filler, Rgb565) { TestFiller<Rgb565>(); }
TEST(Filler, Alpha8) { TestFiller<Alpha8>(color::Black); }
TEST(Filler, Alpha4) { TestFiller<Alpha4>(color::Black); }
TEST(Filler, Indexed1) { TestFiller<Indexed1>(Indexed1(&IndexedPalette1())); }
TEST(Filler, Indexed2) { TestFiller<Indexed2>(Indexed2(&IndexedPalette2())); }
TEST(Filler, Indexed4) { TestFiller<Indexed4>(Indexed4(&IndexedPalette4())); }
TEST(Filler, Indexed8) { TestFiller<Indexed8>(Indexed8(&IndexedPalette8())); }

TEST(Filler, Rgb565WithTransparency) {
  TestWriter<Rgb565WithTransparency>(Rgb565WithTransparency(12));
}

TEST(Offscreen, IndexedDynamicPaletteFillsOnDraw) {
  Palette palette = Palette::Dynamic(16);
  Indexed4 mode(&palette);
  const int16_t width = 2;
  const int16_t height = 2;
  const size_t buffer_size =
      (Indexed4::bits_per_pixel * width * height + 7) / 8;
  std::unique_ptr<roo::byte[]> buffer(new roo::byte[buffer_size]());
  Offscreen<Indexed4> offscreen(width, height, buffer.get(), mode);

  EXPECT_EQ(palette.size(), 0);

  DrawingContext dc(offscreen);
  dc.setBlendingMode(BlendingMode::kSource);
  Color first = Color(0xFF112233);
  Color second = Color(0xFF445566);
  dc.draw(SolidRect(0, 0, 0, 0, first));
  dc.draw(SolidRect(1, 0, 1, 0, second));

  EXPECT_EQ(palette.size(), 2);
  EXPECT_EQ(palette.getColorAt(0), first);
  EXPECT_EQ(palette.getColorAt(1), second);
}

void TestAddressWindowAdvance(uint16_t x0, uint16_t y0, uint16_t x1,
                              uint16_t y1, int16_t raw_width,
                              int16_t raw_height, Orientation orientation,
                              int max_skip) {
  internal::AddressWindow tested;
  internal::AddressWindow reference;
  tested.setAddress(x0, y0, x1, y1, raw_width, raw_height, orientation);
  reference.setAddress(x0, y0, x1, y1, raw_width, raw_height, orientation);
  std::uniform_int_distribution<> distribution(0, max_skip);
  EXPECT_EQ(reference.offset(), tested.offset());
  uint32_t pixels_left = (x1 - x0 + 1) * (y1 - y0 + 1);
  while (pixels_left > 0) {
    uint32_t next = distribution(generator);
    if (next > pixels_left) {
      next = pixels_left;
    }
    for (uint32_t i = 0; i < next; i++) {
      reference.advance();
    }
    tested.advance(next);
    EXPECT_EQ(reference.offset(), tested.offset());
    pixels_left -= next;
  }
}

void TestAddressWindowAdvance(uint16_t x0, uint16_t y0, uint16_t x1,
                              uint16_t y1, int16_t raw_width,
                              int16_t raw_height, int max_skip) {
  Orientation o = Orientation::RightDown();
  for (int i = 0; i < 4; ++i) {
    TestAddressWindowAdvance(x0, y0, x1, y1, raw_width, raw_height, o,
                             max_skip);
    o = o.swapXY();
    TestAddressWindowAdvance(x0, y0, x1, y1, raw_width, raw_height, o,
                             max_skip);
    o = o.swapXY().rotateRight();
  }
}

TEST(AddressWindow, Advance1) {
  TestAddressWindowAdvance(5, 6, 13, 63, 80, 90, 1);
}

TEST(AddressWindow, Advance10) {
  TestAddressWindowAdvance(1, 3, 53, 43, 80, 90, 10);
}

TEST(AddressWindow, Advance200) {
  TestAddressWindowAdvance(1, 3, 53, 43, 80, 90, 200);
}

template <typename ColorMode,
          ColorPixelOrder pixel_order = ColorPixelOrder::kMsbFirst,
          ByteOrder byte_order = roo_io::kBigEndian>
const ConstDramRaster<ColorMode, pixel_order, byte_order> RasterOf(
    const OffscreenDevice<ColorMode, pixel_order, byte_order>& offscreen) {
  return offscreen.raster();
}

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

template <typename ColorMode,
          ColorPixelOrder pixel_order = ColorPixelOrder::kMsbFirst,
          ByteOrder byte_order = roo_io::kBigEndian,
          int8_t pixels_per_byte = ColorTraits<ColorMode>::pixels_per_byte,
          typename storage_type = ColorStorageType<ColorMode>>
class OffscreenDeviceForTest
    : public OffscreenDevice<ColorMode, pixel_order, byte_order,
                             pixels_per_byte, storage_type> {
 public:
  typedef OffscreenDevice<ColorMode, pixel_order, byte_order, pixels_per_byte,
                          storage_type>
      Base;

  OffscreenDeviceForTest(int16_t width, int16_t height, Color bg)
      : Base(
            width, height,
            new roo::byte[(ColorMode::bits_per_pixel * width * height + 7) / 8],
            ColorMode()) {
    Base::fillRect(0, 0, width - 1, height - 1, bg);
  }

  ~OffscreenDeviceForTest() { delete[] Base::buffer(); }
};

template <typename ColorMode>
using RefOffscreen =
    FakeOffscreen<ColorMode, roo_io::kBigEndian, ColorPixelOrder::kMsbFirst>;

TEST_P(OffscreenTest, FillRects) {
  TestFillRects<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, FillHLines) {
  TestFillHLines<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, FillVLines) {
  TestFillVLines<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, FillDegeneratePixels) {
  TestFillDegeneratePixels<OffscreenDeviceForTest<Argb4444>,
                           RefOffscreen<Argb4444>>(std::get<0>(GetParam()),
                                                   std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, FillPixels) {
  TestFillPixels<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WriteRects) {
  TestWriteRects<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WriteHLines) {
  TestWriteHLines<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WriteVLines) {
  TestWriteVLines<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WriteDegeneratePixels) {
  TestWriteDegeneratePixels<OffscreenDeviceForTest<Argb4444>,
                            RefOffscreen<Argb4444>>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WritePixels) {
  TestWritePixels<OffscreenDeviceForTest<Argb4444>, RefOffscreen<Argb4444>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WritePixelsStress) {
  TestWritePixelsStress<OffscreenDeviceForTest<Argb4444>,
                        RefOffscreen<Argb4444>>(std::get<0>(GetParam()),
                                                std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WritePixelsSnake) {
  TestWritePixelsSnake<OffscreenDeviceForTest<Argb4444>,
                       RefOffscreen<Argb4444>>(std::get<0>(GetParam()),
                                               std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WriteRectWindowSimple) {
  TestWriteRectWindowSimple<OffscreenDeviceForTest<Argb4444>,
                            RefOffscreen<Argb4444>>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

TEST_P(OffscreenTest, WriteRectWindowStress) {
  TestWriteRectWindowStress<OffscreenDeviceForTest<Argb4444>,
                            RefOffscreen<Argb4444>>(std::get<0>(GetParam()),
                                                    std::get<1>(GetParam()));
}

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

INSTANTIATE_TEST_SUITE_P(
    OffscreenTestsBlending, OffscreenTest,
    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())));

// Now, let's also test some basic functionality.

TEST_F(OffscreenTest, OffsetedDrawing) {
  Offscreen<Monochrome> offscreen(Box(-6, -5, 4, 3), color::Black,
                                  WhiteOnBlack());
  EXPECT_THAT(offscreen.raster(),
              MatchesContent(WhiteOnBlack(), Box(-6, -5, 4, 3),
                             "           "
                             "           "
                             "           "
                             "           "
                             "           "
                             "           "
                             "           "
                             "           "
                             "           "));

  DrawingContext dc(offscreen);
  EXPECT_FALSE(dc.transformation().is_rescaled());
  EXPECT_FALSE(dc.transformation().is_translated());
  EXPECT_EQ(dc.bounds(), Box(-6, -5, 4, 3));
  EXPECT_EQ(offscreen.extents(), Box(-6, -5, 4, 3));
  EXPECT_EQ(dc.getClipBox(), Box(-6, -5, 4, 3));
  dc.draw(SolidRect(-4, -3, 2, 1, color::White));
  EXPECT_THAT(offscreen.raster(),
              MatchesContent(WhiteOnBlack(), Box(-6, -5, 4, 3),
                             "           "
                             "           "
                             "  *******  "
                             "  *******  "
                             "  *******  "
                             "  *******  "
                             "  *******  "
                             "           "
                             "           "));
  dc.setClipBox(-100, -100, 1, 0);
  EXPECT_EQ(dc.getClipBox(), Box(-6, -5, 1, 0));
  dc.clear();
  EXPECT_THAT(offscreen.raster(),
              MatchesContent(WhiteOnBlack(), Box(-6, -5, 4, 3),
                             "           "
                             "           "
                             "        *  "
                             "        *  "
                             "        *  "
                             "        *  "
                             "  *******  "
                             "           "
                             "           "));
}

TEST_F(OffscreenTest, DrawingOffscreen) {
  Offscreen<Grayscale4> offscreen(Box(-6, -5, 4, 3), color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(-4, -3, 2, 1, Color(0xFF444444)));
  dc.draw(SolidRect(-3, -2, 1, 0, Color(0xFF666666)));
  FakeOffscreen<Argb4444> test_screen(13, 9, color::White);
  Display display(test_screen);
  {
    DrawingContext dc(display);
    dc.draw(offscreen, 5, 4);
  }
  EXPECT_THAT(test_screen, MatchesContent(Grayscale4(), Box(0, 0, 12, 8),
                                          "          ***"
                                          " 4444444  ***"
                                          " 4666664  ***"
                                          " 4666664  ***"
                                          " 4666664  ***"
                                          " 4444444  ***"
                                          "          ***"
                                          "          ***"
                                          "*************"));
}

TEST_F(OffscreenTest, DrawingOffscreenStreamable) {
  Offscreen<Grayscale4> offscreen(Box(-6, -5, 4, 3), color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(-4, -3, 2, 1, Color(0xFF444444)));
  dc.draw(SolidRect(-3, -2, 1, 0, Color(0xFF666666)));
  FakeOffscreen<Argb4444> test_screen(13, 9, color::White);
  Display display(test_screen);
  {
    DrawingContext dc(display);
    dc.draw(ForcedStreamable(&offscreen), 5, 4);
  }
  EXPECT_THAT(test_screen, MatchesContent(Grayscale4(), Box(0, 0, 12, 8),
                                          "          ***"
                                          " 4444444  ***"
                                          " 4666664  ***"
                                          " 4666664  ***"
                                          " 4666664  ***"
                                          " 4444444  ***"
                                          "          ***"
                                          "          ***"
                                          "*************"));
}

TEST_F(OffscreenTest, DrawingOffscreenRasterizable) {
  Offscreen<Grayscale4> offscreen(Box(-6, -5, 4, 3), color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(-4, -3, 2, 1, Color(0xFF444444)));
  dc.draw(SolidRect(-3, -2, 1, 0, Color(0xFF666666)));
  FakeOffscreen<Argb4444> test_screen(13, 9, color::White);
  Display display(test_screen);
  {
    DrawingContext dc(display);
    dc.draw(ForcedRasterizable(&offscreen), 5, 4);
  }
  EXPECT_THAT(test_screen, MatchesContent(Grayscale4(), Box(0, 0, 12, 8),
                                          "          ***"
                                          " 4444444  ***"
                                          " 4666664  ***"
                                          " 4666664  ***"
                                          " 4666664  ***"
                                          " 4444444  ***"
                                          "          ***"
                                          "          ***"
                                          "*************"));
}

const Font& font() { return font_NotoSerif_Italic_12(); }

class Label : public Drawable {
 public:
  Label(const string& label) : label_(label) {}

 private:
  void drawTo(const Surface& s) const override {
    font().drawHorizontalString(s, label_, color::White);
  }

  Box extents() const override {
    return font().getHorizontalStringMetrics(label_).screen_extents();
  }

  std::string label_;
};

TEST_F(OffscreenTest, OffscreenForTranslucentDrawable) {
  Offscreen<Alpha4> offscreen(Label(";"), Alpha4(color::Black));
  DrawingContext dc(offscreen);

  FakeOffscreen<Argb4444> test_screen(10, 7, color::White);
  Display display(test_screen);
  {
    DrawingContext dc(display);
    dc.draw(offscreen, 5, 4);
  }
  EXPECT_THAT(test_screen, MatchesContent(Grayscale4(), Box(0, 0, 9, 6),
                                          "**********"
                                          "**********"
                                          "*****B8***"
                                          "*****A ***"
                                          "******5***"
                                          "*****8E***"
                                          "**********"));
}

TEST_P(OffscreenTest, DrawDirectRect) {
  TestDrawDirectRect<OffscreenDeviceForTest<Rgb565>, RefOffscreen<Rgb565>>(
      std::get<0>(GetParam()), std::get<1>(GetParam()));
}

// ---------- blitCopy tests ----------

TEST_F(OffscreenTest, BlitCopyNonOverlapping) {
  // Byte-aligned color mode (Argb4444, 2 bytes per pixel -> memmove path).
  Offscreen<Argb4444> offscreen(8, 6, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(1, 1, 3, 3, Color(0xFFAAAAAA)));
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 7, 5),
                                                 "        "
                                                 " AAA    "
                                                 " AAA    "
                                                 " AAA    "
                                                 "        "
                                                 "        "));
  offscreen.output().blitCopy(1, 1, 3, 3, 4, 2);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 7, 5),
                                                 "        "
                                                 " AAA    "
                                                 " AAAAAA "
                                                 " AAAAAA "
                                                 "    AAA "
                                                 "        "));
}

TEST_F(OffscreenTest, BlitCopyOverlapDown) {
  // Overlapping copy moving down — requires bottom-to-top iteration.
  Offscreen<Grayscale4> offscreen(4, 6, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(0, 0, 3, 0, Color(0xFF222222)));
  dc.draw(SolidRect(0, 1, 3, 1, Color(0xFF444444)));
  dc.draw(SolidRect(0, 2, 3, 2, Color(0xFF666666)));
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 5),
                                                 "2222"
                                                 "4444"
                                                 "6666"
                                                 "    "
                                                 "    "
                                                 "    "));
  offscreen.output().blitCopy(0, 0, 3, 2, 0, 2);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 5),
                                                 "2222"
                                                 "4444"
                                                 "2222"
                                                 "4444"
                                                 "6666"
                                                 "    "));
}

TEST_F(OffscreenTest, BlitCopyOverlapUp) {
  // Overlapping copy moving up — requires top-to-bottom iteration.
  Offscreen<Grayscale4> offscreen(4, 6, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(0, 3, 3, 3, Color(0xFF222222)));
  dc.draw(SolidRect(0, 4, 3, 4, Color(0xFF444444)));
  dc.draw(SolidRect(0, 5, 3, 5, Color(0xFF666666)));
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 5),
                                                 "    "
                                                 "    "
                                                 "    "
                                                 "2222"
                                                 "4444"
                                                 "6666"));
  offscreen.output().blitCopy(0, 3, 3, 5, 0, 1);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 5),
                                                 "    "
                                                 "2222"
                                                 "4444"
                                                 "6666"
                                                 "4444"
                                                 "6666"));
}

TEST_F(OffscreenTest, BlitCopyOverlapRight) {
  // Same-row overlapping copy moving right — memmove handles correctly.
  Offscreen<Grayscale4> offscreen(8, 1, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(0, 0, 0, 0, Color(0xFF222222)));
  dc.draw(SolidRect(1, 0, 1, 0, Color(0xFF444444)));
  dc.draw(SolidRect(2, 0, 2, 0, Color(0xFF666666)));
  dc.draw(SolidRect(3, 0, 3, 0, Color(0xFF888888)));
  EXPECT_THAT(offscreen.raster(),
              MatchesContent(Grayscale4(), Box(0, 0, 7, 0), "2468    "));
  offscreen.output().blitCopy(0, 0, 3, 0, 2, 0);
  EXPECT_THAT(offscreen.raster(),
              MatchesContent(Grayscale4(), Box(0, 0, 7, 0), "242468  "));
}

TEST_F(OffscreenTest, BlitCopyNoOp) {
  Offscreen<Grayscale4> offscreen(4, 4, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(0, 0, 1, 1, Color(0xFF888888)));
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 3),
                                                 "88  "
                                                 "88  "
                                                 "    "
                                                 "    "));
  // src == dst: early return, no change.
  offscreen.output().blitCopy(0, 0, 1, 1, 0, 0);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 3),
                                                 "88  "
                                                 "88  "
                                                 "    "
                                                 "    "));
}

TEST_F(OffscreenTest, BlitCopyDegenerate) {
  Offscreen<Grayscale4> offscreen(4, 4, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(0, 0, 1, 1, Color(0xFF888888)));
  // src_x1 < src_x0: degenerate rect, no-op.
  offscreen.output().blitCopy(2, 2, 1, 1, 0, 0);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 3),
                                                 "88  "
                                                 "88  "
                                                 "    "
                                                 "    "));
}

TEST_F(OffscreenTest, BlitCopySinglePixel) {
  Offscreen<Grayscale4> offscreen(4, 4, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(1, 1, 1, 1, Color(0xFFAAAAAA)));
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 3),
                                                 "    "
                                                 " A  "
                                                 "    "
                                                 "    "));
  offscreen.output().blitCopy(1, 1, 1, 1, 3, 3);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 3, 3),
                                                 "    "
                                                 " A  "
                                                 "    "
                                                 "   A"));
}

TEST_F(OffscreenTest, BlitCopySubByteUnaligned) {
  // Grayscale4: 2 pixels per byte. src_x0=1 is odd -> general tile fallback.
  Offscreen<Grayscale4> offscreen(8, 3, color::Black);
  DrawingContext dc(offscreen);
  dc.draw(SolidRect(1, 0, 1, 0, Color(0xFF444444)));
  dc.draw(SolidRect(2, 0, 2, 0, Color(0xFF666666)));
  dc.draw(SolidRect(3, 0, 3, 0, Color(0xFF888888)));
  dc.draw(SolidRect(1, 1, 1, 1, Color(0xFFAAAAAA)));
  dc.draw(SolidRect(2, 1, 2, 1, Color(0xFFCCCCCC)));
  dc.draw(SolidRect(3, 1, 3, 1, Color(0xFFEEEEEE)));
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 7, 2),
                                                 " 468    "
                                                 " ACE    "
                                                 "        "));
  offscreen.output().blitCopy(1, 0, 3, 1, 4, 1);
  EXPECT_THAT(offscreen.raster(), MatchesContent(Grayscale4(), Box(0, 0, 7, 2),
                                                 " 468    "
                                                 " ACE468 "
                                                 "    ACE "));
}

}  // namespace roo_display
