#pragma once

#include "roo_display/hal/config.h"

#if !defined(ESP_PLATFORM) || !(CONFIG_IDF_TARGET_ESP32S3)
#warning Compilation target must be ESP32_S3 for this device.
#else

#include <memory>

#include "esp_lcd_types.h"
#include "rom/cache.h"
#include "roo_display/color/blending.h"
#include "roo_display/core/device.h"
#include "roo_display/core/offscreen.h"
#include "roo_display/internal/byte_order.h"
#include "roo_display/internal/color_format.h"
#include "roo_display/internal/color_io.h"

namespace roo_display {

namespace internal {

class Rgb565Dma : public Rgb565 {};

template <>
struct ColorFormatTraits<Rgb565Dma> {
  static constexpr const DisplayOutput::ColorFormat::Mode mode =
      DisplayOutput::ColorFormat::kModeRgb565;
};

}  // namespace internal

template <roo_io::ByteOrder byte_order>
struct RawFullByteBlender<internal::Rgb565Dma, BlendingMode::kSourceOver,
                          byte_order> {
  inline void operator()(roo::byte* dst, Color src,
                         const internal::Rgb565Dma& mode) const {
    ColorIo<internal::Rgb565Dma, byte_order> io;
    io.store(AlphaBlendOverOpaque(io.load(dst, mode), src), dst, mode);
  }
};

namespace internal {

template <BlendingMode blending_mode>
class Rgb565DmaBlendingWriterOperator {
 public:
  Rgb565DmaBlendingWriterOperator(Rgb565Dma color_mode, const Color* color)
      : color_mode_(color_mode), color_(color) {}

  void operator()(roo::byte* p, uint32_t offset) {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    RawFullByteBlender<Rgb565Dma, blending_mode, roo_io::kLittleEndian> blender;
    blender(cursor, *color_++, color_mode_);
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         kBytesPerPixel);
  }

  void operator()(roo::byte* p, uint32_t offset, uint32_t count) {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    RawFullByteBlender<Rgb565Dma, blending_mode, roo_io::kLittleEndian> blender;
    uint32_t orig_count = count;
    while (count-- > 0) {
      blender(cursor, *color_++, color_mode_);
      cursor += kBytesPerPixel;
    }
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         orig_count * kBytesPerPixel);
  }

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

template <>
struct BlendingWriter<Rgb565Dma, ColorPixelOrder::kMsbFirst,
                      roo_io::kLittleEndian> {
  template <BlendingMode blending_mode>
  using Operator = Rgb565DmaBlendingWriterOperator<blending_mode>;
};

template <>
class GenericWriter<Rgb565Dma, ColorPixelOrder::kMsbFirst,
                    roo_io::kLittleEndian, 1, uint16_t> {
 public:
  GenericWriter(const Rgb565Dma& color_mode, BlendingMode blending_mode,
                Color* color)
      : color_(color), blending_mode_(blending_mode) {}

  void operator()(roo::byte* p, uint32_t offset) {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    ApplyRawFullByteBlending<Rgb565Dma, roo_io::kLittleEndian>(
        blending_mode_, cursor, *color_++, Rgb565Dma());
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         kBytesPerPixel);
  }

  void operator()(roo::byte* p, uint32_t offset, uint32_t count) {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    uint32_t orig_count = count;
    while (count-- > 0) {
      ApplyRawFullByteBlending<Rgb565Dma, roo_io::kLittleEndian>(
          blending_mode_, cursor, *color_++, Rgb565Dma());
      cursor += kBytesPerPixel;
    }
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         orig_count * kBytesPerPixel);
  }

 private:
  Color* color_;
  BlendingMode blending_mode_;
};

template <BlendingMode blending_mode>
class Rgb565DmaBlendingFillerOperator {
 public:
  Rgb565DmaBlendingFillerOperator(Rgb565Dma color_mode, Color color)
      : color_(color) {}

  void operator()(roo::byte* p, uint32_t offset) const {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    RawFullByteBlender<Rgb565Dma, blending_mode, roo_io::kLittleEndian> blender;
    blender(cursor, color_, Rgb565Dma());
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         kBytesPerPixel);
  }

  void operator()(roo::byte* p, uint32_t offset, uint32_t count) const {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    RawFullByteBlender<Rgb565Dma, blending_mode, roo_io::kLittleEndian> blender;
    uint32_t orig_count = count;
    while (count-- > 0) {
      blender(cursor, color_, Rgb565Dma());
      cursor += kBytesPerPixel;
    }
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         orig_count * kBytesPerPixel);
  }

 private:
  Color color_;
  BlendingMode blending_mode_;
};

template <>
struct BlendingFiller<Rgb565Dma, ColorPixelOrder::kMsbFirst,
                      roo_io::kLittleEndian> {
  template <BlendingMode blending_mode>
  using Operator = Rgb565DmaBlendingFillerOperator<blending_mode>;
};

template <>
class GenericFiller<Rgb565Dma, ColorPixelOrder::kMsbFirst,
                    roo_io::kLittleEndian, 1, uint16_t> {
 public:
  GenericFiller(const Rgb565Dma& color_mode, BlendingMode blending_mode,
                Color color)
      : color_(color), blending_mode_(blending_mode) {}

  void operator()(roo::byte* p, uint32_t offset) const {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    ApplyRawFullByteBlending<Rgb565Dma, roo_io::kLittleEndian>(
        blending_mode_, cursor, color_, Rgb565Dma());
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         kBytesPerPixel);
  }

  void operator()(roo::byte* p, uint32_t offset, uint32_t count) const {
    constexpr uint32_t kBytesPerPixel = ColorTraits<Rgb565Dma>::bytes_per_pixel;
    roo::byte* cursor = p + offset * kBytesPerPixel;
    uint32_t orig_count = count;
    while (count-- > 0) {
      ApplyRawFullByteBlending<Rgb565Dma, roo_io::kLittleEndian>(
          blending_mode_, cursor, color_, Rgb565Dma());
      cursor += kBytesPerPixel;
    }
    Cache_WriteBack_Addr((uint32_t)(p) + offset * kBytesPerPixel,
                         orig_count * kBytesPerPixel);
  }

 private:
  Color color_;
  BlendingMode blending_mode_;
};

}  // namespace internal

// Note: this one is mainly for performance tests and it doesn't support screen
// rotation or alpha-blending.
// #define FLUSH_MODE_HARDCODED

namespace esp32s3_dma {

enum FlushMode {
  // Writes back after every memory write. Slow.
  FLUSH_MODE_AGGRESSIVE = 0,

  // Writes back after every driver call, i.e. per some number of pixels or
  // rectangles.
  FLUSH_MODE_BUFFERED = 1,

  // Only writes back at the end of transaction (in device.end()). In theory,
  // that sounds like good
  // idea. And indeed, it seems the fastest. In practice, for some reason it
  // causes displays to lose synchronization quite a lot.
  FLUSH_MODE_LAZY = 2,
};

namespace internal {

template <FlushMode>
struct Traits {
  using ColorMode = Rgb565;
};

template <>
struct Traits<FLUSH_MODE_AGGRESSIVE> {
  using ColorMode = ::roo_display::internal::Rgb565Dma;
};

}  // namespace internal

struct Config {
  uint16_t width;
  uint16_t height;
  int8_t de;
  int8_t hsync;
  int8_t vsync;
  int8_t pclk;
  uint16_t hsync_pulse_width;
  uint16_t hsync_back_porch;
  uint16_t hsync_front_porch;
  uint16_t hsync_polarity;
  uint16_t vsync_pulse_width;
  uint16_t vsync_back_porch;
  uint16_t vsync_front_porch;
  uint16_t vsync_polarity;
  uint16_t pclk_active_neg;
  int32_t prefer_speed;

  int8_t r0;
  int8_t r1;
  int8_t r2;
  int8_t r3;
  int8_t r4;
  int8_t g0;
  int8_t g1;
  int8_t g2;
  int8_t g3;
  int8_t g4;
  int8_t g5;
  int8_t b0;
  int8_t b1;
  int8_t b2;
  int8_t b3;
  int8_t b4;
  bool bswap;
};

struct AllocatedPanel {
  roo::byte* buffer;
  esp_lcd_panel_handle_t handle;
};

AllocatedPanel AllocatePanel(const Config& config);

template <FlushMode flush_mode>
class ParallelRgb565 : public DisplayDevice {
 public:
  using ColorMode = Rgb565;
  static constexpr ColorPixelOrder pixel_order = ColorPixelOrder::kMsbFirst;
  static constexpr ByteOrder byte_order = roo_io::kLittleEndian;

  ParallelRgb565(Config cfg)
      : DisplayDevice(cfg.width, cfg.height), cfg_(std::move(cfg)) {}

  void init() override;

  void begin() override {}

  void end() override;

  void setAddress(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
                  BlendingMode mode) override {
    buffer_->setAddress(x0, y0, x1, y1, mode);
  }

  void write(Color* color, uint32_t pixel_count) override;

  void fill(Color color, uint32_t pixel_count) override;

  void writePixels(BlendingMode mode, Color* color, int16_t* x, int16_t* y,
                   uint16_t pixel_count) override;

  void fillPixels(BlendingMode mode, Color color, int16_t* x, int16_t* y,
                  uint16_t pixel_count) override;

  void writeRects(BlendingMode mode, Color* color, int16_t* x0, int16_t* y0,
                  int16_t* x1, int16_t* y1, uint16_t count) override;

  void fillRects(BlendingMode mode, Color color, int16_t* x0, int16_t* y0,
                 int16_t* x1, int16_t* y1, uint16_t count) override;

  void drawDirectRect(const roo::byte* data, size_t row_width_bytes,
                      int16_t src_x0, int16_t src_y0, int16_t src_x1,
                      int16_t src_y1, int16_t dst_x0, int16_t dst_y0) override;

  void drawDirectRectAsync(const roo::byte* data, size_t row_width_bytes,
                           int16_t src_x0, int16_t src_y0, int16_t src_x1,
                           int16_t src_y1, int16_t dst_x0,
                           int16_t dst_y0) override;

  const ColorFormat& getColorFormat() const override {
    static const ::roo_display::internal::ColorFormatImpl<
        Rgb565, roo_io::kLittleEndian, ColorPixelOrder::kMsbFirst>
        format(color_mode());
    return format;
  }

  const Capabilities& getCapabilities() const override {
    static const Capabilities kCaps(/*supports_blending=*/true,
                                    /*supports_blit_copy=*/true);
    return kCaps;
  }

  void blitCopy(int16_t src_x0, int16_t src_y0, int16_t src_x1,
                int16_t src_y1, int16_t dst_x0, int16_t dst_y0) override {
    if (buffer_ != nullptr) {
      buffer_->blitCopy(src_x0, src_y0, src_x1, src_y1, dst_x0, dst_y0);
    }
  }

  const Rgb565& color_mode() const {
    static const Rgb565 mode;
    return mode;
  }

  void orientationUpdated() override {
    if (buffer_ != nullptr) {
      buffer_->setOrientation(orientation());
    }
  }

  void flush() override {
    if (buffer_ != nullptr) {
      buffer_->flush();
    }
    flushCache();
  }

 private:
  inline void flushCache() {
    uint32_t begin = (uint32_t)buffer_->buffer();
    uint32_t size = cfg_.width * cfg_.height * 2;
    Cache_WriteBack_Addr(begin, size);
  }

  using Dev =
      OffscreenDevice<typename internal::Traits<flush_mode>::ColorMode,
                      ColorPixelOrder::kMsbFirst, roo_io::kLittleEndian>;

  Config cfg_;
  esp_lcd_panel_handle_t panel_handle_{nullptr};
  std::unique_ptr<Dev> buffer_;
};

using ParallelRgb565Buffered = ParallelRgb565<FLUSH_MODE_BUFFERED>;

}  // namespace esp32s3_dma

}  // namespace roo_display

#endif  // #if defined(ESP32) && (CONFIG_IDF_TARGET_ESP32S3)
