#ifndef PANEL_DEVICE_H
#define PANEL_DEVICE_H

#include <esp_lcd_panel_dev.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_io.h>
#include <esp_log.h>

#include <display/lv_display.h>

#include "i2c.h"

// LVGL reserves 2x4 bytes in the buffer to be used as a palette.
// This additional space must be added to the IPanelDevice::buf_size_.
#define LVGL_PALETTE_SIZE      8

/**
 * Encapsulates vendor specific ESP LCD pabel initialization logic.
 * This pure virtual interface can be inherited from for using new LCD devices.
 * See the SSD1306 example for implementing a PanelDevice for NT35510 or ST7789.
 *
 * At this time only I2C is supported.
 * Classes that inherit from this interface should likely be marked final.
 */
class IPanelDevice {
public:
  /**
   * Construct an IPanelDevice.
   *
   * @param i2c I2C object. Eventually this will mature to IProtocol or similar.
   * @param config I2C configuration for this device.
   * @param height Height of the device screen in pixels.
   * @param width Width of the device screen in pixels.
   */
  explicit IPanelDevice(I2C &i2c,
                        esp_lcd_panel_io_i2c_config_t config,
                        int width,
                        int height) :
      IPanelDevice(i2c, config, width, height,
                   width * height / 8 + LVGL_PALETTE_SIZE) { }

  /**
   * Construct an IPanelDevice.
   *
   * @param i2c I2C object. Eventually this will mature to IProtocol or similar.
   * @param config I2C configuration for this device.
   * @param height Height of the device screen in pixels.
   * @param width Width of the device screen in pixels.
   * @param draw_buf_size Size of the draw buffer for this device.
   */
  explicit IPanelDevice(I2C &i2c,
                        esp_lcd_panel_io_i2c_config_t io_config,
                        int width,
                        int height,
                        size_t draw_buf_size) :
      width_(width),
      height_(height),
      rst_num_(i2c.rst_num_),
      lv_buf_size_(draw_buf_size),
      esp_io_config_(io_config) { }

  virtual ~IPanelDevice() = default;

  /**
   * Create an LVGL display using the width and height of this device.
   *
   * @return Handle to the created LVGL display.
   */
  [[nodiscard]] lv_display_t *create_display() const
  {
    auto display = lv_display_create(width_, height_);
    assert(display);
    return display;
  }

  /**
   * Create an ESP LCD panel IO handle.
   *
   * @return The created ESP LCD panel IO handle.
   */
  [[nodiscard]] esp_lcd_panel_io_handle_t create_io_handle()
  {
    ESP_LOGI(TAG, "Creating panel IO handle");
    esp_lcd_panel_io_handle_t handle = nullptr;
    ESP_ERROR_CHECK(
        esp_lcd_new_panel_io_i2c(I2C::get(), &esp_io_config_, &handle));
    return handle;
  }

  /**
   * Create and initialize an ESP panel handle.
   * IPanelDevice implementors must initialize the panel within init_panel.
   *
   * @param config ESP LCD panel configuration.
   * @param io ESP LCD panel IO handle.
   * @param [out] panel ESP LCD panel handle output pointer location.
   */
  void create_panel(esp_lcd_panel_dev_config_t &config,
                    esp_lcd_panel_io_handle_t io,
                    esp_lcd_panel_handle_t &panel)
  {
    // If the passed handle is already allocated, delete it.
    if (panel != nullptr) {
      ESP_LOGI(TAG, "Removing unused panel");
      esp_lcd_panel_del(panel);
    }

    ESP_LOGI(TAG, "Install SSD1306 panel driver");
    // Call pure virtual method responsible for initializing the panel handle.
    init_panel(config, io, panel);

  }

  /**
   * Retrieve the device specific vendor configuration structure.
   *
   * @return Address of vendor configuration structure.
   * @sa SSD1306::vendor_config
   */
  virtual void *vendor_config() = 0;

  /// Width of the device screen in pixels.
  int32_t width_;

  /// Height of the device screen in pixels.
  int32_t height_;

  /// RST GPIO pin number.
  int rst_num_;

  /// LVGL draw buffer size for the device.
  size_t lv_buf_size_;

  /// ESP LCD panel IO configuration.
  esp_lcd_panel_io_i2c_config_t esp_io_config_;

private:
  /**
   * Initializes the ESP panel using vendor specific APIs and configurations.
   * This method should implement any setup logic specific to the device.
   *
   * @param config ESP LCD panel configuration.
   * @param io ESP LCD panel IO handle.
   * @param [out] panel ESP LCD panel handle output pointer location.
   */
  virtual void init_panel(esp_lcd_panel_dev_config_t &config,
                          esp_lcd_panel_io_handle_t io,
                          esp_lcd_panel_handle_t &panel) = 0;
};

#endif // PANEL_DEVICE_H
