/*
 * OV5640 DVP via esp_camera + ST7789 SPI LCD (240x320 panel, 240x240 visible)
 * QVGA capture, 2x2 box-averaged down to 240x180 for a clean, sharp image.
 */
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "esp_camera.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"

static const char *TAG = "cam_project";

/* ── Tunables ────────────────────────────────────────────────────────────── */
#define SWAP_BYTES     1     /* RGB565 byte order */
#define PANEL_BGR      0     /* 0 = RGB, 1 = BGR */
#define AUTO_EXPOSURE  0     /* 1 = adaptive, 0 = fixed EXPOSURE_VAL */
#define EXPOSURE_VAL   200   /* used only when AUTO_EXPOSURE = 0 */

/* ── Camera pins ─────────────────────────────────────────────────────────── */
#define CAM_PIN_PWDN    -1
#define CAM_PIN_RESET   -1
#define CAM_PIN_XCLK    10
#define CAM_PIN_SIOD    5
#define CAM_PIN_SIOC    4
#define CAM_PIN_D7      40
#define CAM_PIN_D6      48
#define CAM_PIN_D5      45
#define CAM_PIN_D4      9
#define CAM_PIN_D3      8
#define CAM_PIN_D2      17
#define CAM_PIN_D1      7
#define CAM_PIN_D0      6
#define CAM_PIN_VSYNC   14
#define CAM_PIN_HREF    16
#define CAM_PIN_PCLK    13

/* ── LCD pins ────────────────────────────────────────────────────────────── */
#define LCD_MOSI    11
#define LCD_SCLK    12
#define LCD_CS      21
#define LCD_DC      47
#define LCD_RST     -1
#define LCD_BL      18

#define LCD_W       240
#define LCD_H       240
#define PANEL_H     320
#define LCD_GAP_Y   80

/* Capture QVGA (4:3), display 240x180 (4:3) — pure downscale, no distortion */
#define SRC_W       320
#define SRC_H       240
#define VIEW_W      240
#define VIEW_H      180
#define VIEW_Y0     ((LCD_H - VIEW_H) / 2)   /* = 30 */

static esp_lcd_panel_handle_t s_panel = NULL;
static uint16_t s_fb[VIEW_W * VIEW_H];   /* scaled output image */
static uint16_t s_black[LCD_W];

static inline uint16_t rd565(const uint8_t *p)
{
#if SWAP_BYTES
    return ((uint16_t)p[1] << 8) | p[0];
#else
    return ((uint16_t)p[0] << 8) | p[1];
#endif
}

/* ── LCD init ────────────────────────────────────────────────────────────── */
static void lcd_init(void)
{
    ledc_timer_config_t lt = {
        .speed_mode      = LEDC_LOW_SPEED_MODE,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .timer_num       = LEDC_TIMER_1,
        .freq_hz         = 5000,
        .clk_cfg         = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&lt);

    ledc_channel_config_t lc = {
        .gpio_num            = LCD_BL,
        .speed_mode          = LEDC_LOW_SPEED_MODE,
        .channel             = LEDC_CHANNEL_0,
        .timer_sel           = LEDC_TIMER_1,
        .duty                = 0,
        .hpoint              = 0,
        .flags.output_invert = false,
    };
    ledc_channel_config(&lc);

    spi_bus_config_t bc = {
        .mosi_io_num     = LCD_MOSI,
        .miso_io_num     = -1,
        .sclk_io_num     = LCD_SCLK,
        .quadwp_io_num   = -1,
        .quadhd_io_num   = -1,
        .max_transfer_sz = VIEW_W * VIEW_H * 2 + 64,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bc, SPI_DMA_CH_AUTO));

    esp_lcd_panel_io_handle_t io = NULL;
    esp_lcd_panel_io_spi_config_t ic = {
        .dc_gpio_num       = LCD_DC,
        .cs_gpio_num       = LCD_CS,
        .pclk_hz           = 80000000,
        .lcd_cmd_bits      = 8,
        .lcd_param_bits    = 8,
        .spi_mode          = 0,
        .trans_queue_depth = 10,
    };
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &ic, &io));

    esp_lcd_panel_dev_config_t pc = {
        .reset_gpio_num = LCD_RST,
#if PANEL_BGR
        .rgb_ele_order  = LCD_RGB_ELEMENT_ORDER_BGR,
#else
        .rgb_ele_order  = LCD_RGB_ELEMENT_ORDER_RGB,
#endif
        .bits_per_pixel = 16,
    };
    ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io, &pc, &s_panel));
    ESP_ERROR_CHECK(esp_lcd_panel_init(s_panel));
    esp_lcd_panel_invert_color(s_panel, true);
    esp_lcd_panel_disp_on_off(s_panel, true);

    esp_lcd_panel_set_gap(s_panel, 0, 0);
    for (int i = 0; i < LCD_W; i++) s_black[i] = 0x0000;
    for (int y = 0; y < PANEL_H; y++)
        esp_lcd_panel_draw_bitmap(s_panel, 0, y, LCD_W, y + 1, s_black);
    esp_lcd_panel_set_gap(s_panel, 0, LCD_GAP_Y);

    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 1023);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
    ESP_LOGI(TAG, "LCD init OK");
}

/* ── Camera init ─────────────────────────────────────────────────────────── */
static bool camera_init(void)
{
    camera_config_t cfg = {
        .pin_pwdn       = CAM_PIN_PWDN,
        .pin_reset      = CAM_PIN_RESET,
        .pin_xclk       = CAM_PIN_XCLK,
        .pin_sccb_sda   = CAM_PIN_SIOD,
        .pin_sccb_scl   = CAM_PIN_SIOC,
        .pin_d7         = CAM_PIN_D7,
        .pin_d6         = CAM_PIN_D6,
        .pin_d5         = CAM_PIN_D5,
        .pin_d4         = CAM_PIN_D4,
        .pin_d3         = CAM_PIN_D3,
        .pin_d2         = CAM_PIN_D2,
        .pin_d1         = CAM_PIN_D1,
        .pin_d0         = CAM_PIN_D0,
        .pin_vsync      = CAM_PIN_VSYNC,
        .pin_href       = CAM_PIN_HREF,
        .pin_pclk       = CAM_PIN_PCLK,
        .xclk_freq_hz   = 16000000,
        .ledc_timer     = LEDC_TIMER_0,
        .ledc_channel   = LEDC_CHANNEL_1,
        .pixel_format   = PIXFORMAT_RGB565,
        .frame_size     = FRAMESIZE_QVGA,           /* 320x240 */
        .jpeg_quality   = 12,
        .fb_count       = 2,
        .fb_location    = CAMERA_FB_IN_PSRAM,        /* QVGA needs PSRAM */
        .grab_mode      = CAMERA_GRAB_WHEN_EMPTY,
    };

    esp_err_t err = esp_camera_init(&cfg);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Camera init failed: 0x%x", err);
        return false;
    }

    sensor_t *s = esp_camera_sensor_get();
    if (s) {
        s->set_hmirror(s, 1);
        s->set_vflip(s, 1);
        s->set_whitebal(s, 1);
        s->set_awb_gain(s, 1);
        s->set_wb_mode(s, 0);
        s->set_aec2(s, 0);
        s->set_exposure_ctrl(s, 1);
        s->set_gain_ctrl(s, 1);
        s->set_gainceiling(s, GAINCEILING_2X);
        s->set_brightness(s, 0);
        s->set_contrast(s, 1);
        s->set_saturation(s, 1);      /* was 2 — lower cuts color speckle */

        /* Image-quality corrections — clean up noise/vignette */
        s->set_bpc(s, 1);             /* bad-pixel correction */
        s->set_wpc(s, 1);             /* white-pixel correction */
        s->set_raw_gma(s, 1);         /* gamma */
        s->set_lenc(s, 1);            /* lens shading correction */
        s->set_dcw(s, 1);             /* downsample smoothing */
    }

    ESP_LOGI(TAG, "Camera init OK");
    return true;
}

#if !AUTO_EXPOSURE
static void lock_exposure(void)
{
    sensor_t *s = esp_camera_sensor_get();
    if (!s) return;
    s->set_exposure_ctrl(s, 0);
    s->set_aec_value(s, EXPOSURE_VAL);
    s->set_gain_ctrl(s, 0);
    s->set_agc_gain(s, 0);
    ESP_LOGI(TAG, "Exposure locked (aec_value=%d)", EXPOSURE_VAL);
}
#endif

/* ── app_main ────────────────────────────────────────────────────────────── */
void app_main(void)
{
    lcd_init();

    if (!camera_init()) {
        ESP_LOGE(TAG, "Camera failed — halting");
        return;
    }

    for (int i = 0; i < 40; i++) {
        camera_fb_t *fb = esp_camera_fb_get();
        if (fb) esp_camera_fb_return(fb);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
#if !AUTO_EXPOSURE
    lock_exposure();
    for (int i = 0; i < 10; i++) {
        camera_fb_t *fb = esp_camera_fb_get();
        if (fb) esp_camera_fb_return(fb);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
#endif
    ESP_LOGI(TAG, "Starting render");

    uint32_t frames = 0;
    while (1) {
        camera_fb_t *fb = esp_camera_fb_get();
        if (!fb) { vTaskDelay(pdMS_TO_TICKS(5)); continue; }

        if (fb->len < (size_t)(SRC_W * SRC_H * 2)) {
            esp_camera_fb_return(fb);
            continue;
        }

        const uint8_t *buf = fb->buf;

        /* Downscale 320x240 → 240x180 with 2x2 box averaging (denoise + sharpen) */
        for (int oy = 0; oy < VIEW_H; oy++) {
            int sy = oy * SRC_H / VIEW_H;
            if (sy > SRC_H - 2) sy = SRC_H - 2;
            const uint8_t *r0 = buf + (size_t)sy * SRC_W * 2;
            const uint8_t *r1 = r0 + SRC_W * 2;
            uint16_t *dst = &s_fb[oy * VIEW_W];

            for (int ox = 0; ox < VIEW_W; ox++) {
                int sx = ox * SRC_W / VIEW_W;
                if (sx > SRC_W - 2) sx = SRC_W - 2;
                const uint8_t *a = r0 + sx * 2;
                const uint8_t *b = r1 + sx * 2;
                uint16_t p0 = rd565(a),     p1 = rd565(a + 2);
                uint16_t p2 = rd565(b),     p3 = rd565(b + 2);

                uint32_t r = ((p0 >> 11) & 0x1F) + ((p1 >> 11) & 0x1F)
                           + ((p2 >> 11) & 0x1F) + ((p3 >> 11) & 0x1F);
                uint32_t g = ((p0 >> 5) & 0x3F) + ((p1 >> 5) & 0x3F)
                           + ((p2 >> 5) & 0x3F) + ((p3 >> 5) & 0x3F);
                uint32_t bl = (p0 & 0x1F) + (p1 & 0x1F)
                            + (p2 & 0x1F) + (p3 & 0x1F);

                dst[ox] = (uint16_t)(((r >> 2) << 11) | ((g >> 2) << 5) | (bl >> 2));
            }
        }

        esp_camera_fb_return(fb);
        esp_lcd_panel_draw_bitmap(s_panel, 0, VIEW_Y0, VIEW_W, VIEW_Y0 + VIEW_H, s_fb);

        frames++;
        if (frames % 30 == 0) ESP_LOGI(TAG, "Frames: %lu", frames);
    }
}