// Note: Run menuconfig to and set the right EPD display and board for your epaper.

// This example does not use a decoded buffer hence leaves more external RAM free
// and it uses a different JPEG decoder: https://github.com/bitbank2/JPEGDEC
// This decoder is not included and should be placed in the components directory
// Check this for a reference on how to do it :
// https://github.com/martinberlin/cale-idf/tree/master/main/www-jpg-render Adding this
// CMakeLists.txt file is required:
/*
set(srcs
    "JPEGDEC.cpp"
    "jpeg.c"
)
idf_component_register(SRCS ${srcs}
                    REQUIRES "jpegdec"
                    INCLUDE_DIRS "include")
*/
// Open settings to set WiFi and other configurations for both examples:
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "esp_task_wdt.h"
#include "esp_timer.h"
#include "esp_types.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
#include "settings.h"
// WiFi related
#include "esp_event.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "nvs_flash.h"
// HTTP Client + time
#include "esp_http_client.h"
#include "esp_netif.h"
#include "esp_sntp.h"
// C
#include <math.h>  // round + pow
#include <stdio.h>
#include <string.h>
extern "C" {
#include "epd_highlevel.h"
#include "epdiy.h"
}
EpdiyHighlevelState hl;
// JPG decoder from @bitbank2
#include "JPEGDEC.h"

JPEGDEC jpeg;
// EXPERIMENTAL: If JPEG_CPY_FRAMEBUFFER is true the JPG is decoded directly in EPD framebuffer
// On true it looses rotation. Experimental, does not work alright yet. Hint:
// Check if an uint16_t buffer can be copied in a uint8_t buffer directly
#define JPEG_CPY_FRAMEBUFFER true

// Dither space allocation
uint8_t* dither_space;

// Internal array for gamma grayscale
uint8_t gamme_curve[256];

extern "C" {
void app_main();
}

// Load the EMBED_TXTFILES. Then doing (char*) server_cert_pem_start you get the SSL certificate
// Reference:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system.html#embedding-binary-data
extern const uint8_t server_cert_pem_start[] asm("_binary_server_cert_pem_start");

// Buffers
uint8_t* fb;          // EPD 2bpp buffer
uint8_t* source_buf;  // JPG download buffer

uint32_t buffer_pos = 0;
uint32_t time_download = 0;
uint32_t time_decomp = 0;
uint32_t time_render = 0;
uint16_t ep_width = 0;
uint16_t ep_height = 0;

static const char* TAG = "Jpgdec";
uint16_t countDataEventCalls = 0;
uint32_t countDataBytes = 0;
uint32_t img_buf_pos = 0;
uint64_t startTime = 0;

#if VALIDATE_SSL_CERTIFICATE == true
/* Time aware for ESP32: Important to check SSL certs validity */
void time_sync_notification_cb(struct timeval* tv) {
    ESP_LOGI(TAG, "Notification of a time synchronization event");
}

static void initialize_sntp(void) {
    ESP_LOGI(TAG, "Initializing SNTP");
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_set_time_sync_notification_cb(time_sync_notification_cb);
#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_SMOOTH
    sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
#endif
    sntp_init();
}

static void obtain_time(void) {
    initialize_sntp();

    // wait for time to be set
    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
    time(&now);
    localtime_r(&now, &timeinfo);
}
#endif
//====================================================================================
// This sketch contains support functions to render the Jpeg images
//
// Created by Bitbank
// Refactored by @martinberlin for EPDiy as a Jpeg download and render example
//====================================================================================

/*
 * Used with jpeg.setPixelType(FOUR_BIT_DITHERED)
 */
uint16_t mcu_count = 0;
int JPEGDraw4Bits(JPEGDRAW* pDraw) {
    uint32_t render_start = esp_timer_get_time();

#if JPEG_CPY_FRAMEBUFFER
    // Highly experimental: Does not support rotation and gamma correction
    // Can be washed out compared to JPEG_CPY_FRAMEBUFFER false
    for (uint16_t yy = 0; yy < pDraw->iHeight; yy++) {
        // Copy directly horizontal MCU pixels in EPD fb
        memcpy(
            &fb[(pDraw->y + yy) * epd_width() / 2 + pDraw->x / 2],
            &pDraw->pPixels[(yy * pDraw->iWidth) >> 2],
            pDraw->iWidth
        );
    }

#else
    // Rotation aware
    for (int16_t xx = 0; xx < pDraw->iWidth; xx += 4) {
        for (int16_t yy = 0; yy < pDraw->iHeight; yy++) {
            uint16_t col = pDraw->pPixels[(xx + (yy * pDraw->iWidth)) >> 2];

            uint8_t col1 = col & 0xf;
            uint8_t col2 = (col >> 4) & 0xf;
            uint8_t col3 = (col >> 8) & 0xf;
            uint8_t col4 = (col >> 12) & 0xf;
            epd_draw_pixel(pDraw->x + xx, pDraw->y + yy, gamme_curve[col1 * 16], fb);
            epd_draw_pixel(pDraw->x + xx + 1, pDraw->y + yy, gamme_curve[col2 * 16], fb);
            epd_draw_pixel(pDraw->x + xx + 2, pDraw->y + yy, gamme_curve[col3 * 16], fb);
            epd_draw_pixel(pDraw->x + xx + 3, pDraw->y + yy, gamme_curve[col4 * 16], fb);

            /* if (yy==0 && mcu_count==0) {
              printf("1.%d %d %d %d ",col1,col2,col3,col4);
            } */
        }
    }
#endif

    mcu_count++;
    time_render += (esp_timer_get_time() - render_start) / 1000;
    return 1;
}

void deepsleep() {
    esp_deep_sleep(1000000LL * 60 * DEEPSLEEP_MINUTES_AFTER_RENDER);
}

//====================================================================================
//   This function opens source_buf Jpeg image file and primes the decoder
//====================================================================================
int decodeJpeg(uint8_t* source_buf, int xpos, int ypos) {
    uint32_t decode_start = esp_timer_get_time();

    if (jpeg.openRAM(source_buf, img_buf_pos, JPEGDraw4Bits)) {
        jpeg.setPixelType(FOUR_BIT_DITHERED);

        if (jpeg.decodeDither(dither_space, 0)) {
            time_decomp = (esp_timer_get_time() - decode_start) / 1000 - time_render;
            ESP_LOGI(
                "decode",
                "%d ms - %dx%d image MCUs:%d",
                time_decomp,
                jpeg.getWidth(),
                jpeg.getHeight(),
                mcu_count
            );
        } else {
            ESP_LOGE("jpeg.decode", "Failed with error: %d", jpeg.getLastError());
        }

    } else {
        ESP_LOGE("jpeg.openRAM", "Failed with error: %d", jpeg.getLastError());
    }
    jpeg.close();

    return 1;
}

// Handles Htpp events and is in charge of buffering source_buf (jpg compressed image)
esp_err_t _http_event_handler(esp_http_client_event_t* evt) {
    switch (evt->event_id) {
        case HTTP_EVENT_ERROR:
            ESP_LOGE(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGI(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
#if DEBUG_VERBOSE
            ESP_LOGI(
                TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value
            );
#endif
            break;
        case HTTP_EVENT_ON_DATA:
            ++countDataEventCalls;
#if DEBUG_VERBOSE
            if (countDataEventCalls % 10 == 0) {
                ESP_LOGI(TAG, "%d len:%d\n", countDataEventCalls, evt->data_len);
            }
#endif

            if (countDataEventCalls == 1)
                startTime = esp_timer_get_time();
            // Append received data into source_buf
            memcpy(&source_buf[img_buf_pos], evt->data, evt->data_len);
            img_buf_pos += evt->data_len;
            break;

        case HTTP_EVENT_ON_FINISH:
            // Do not draw if it's a redirect (302)
            if (esp_http_client_get_status_code(evt->client) == 200) {
                printf("%d bytes read from %s\n", img_buf_pos, IMG_URL);
                time_download = (esp_timer_get_time() - startTime) / 1000;

                decodeJpeg(source_buf, 0, 0);

                ESP_LOGI("www-dw", "%d ms - download", time_download);
                ESP_LOGI(
                    "render",
                    "%d ms - copying pix (JPEG_CPY_FRAMEBUFFER:%d)",
                    time_render,
                    JPEG_CPY_FRAMEBUFFER
                );
                // Refresh display
                epd_hl_update_screen(&hl, MODE_GC16, 25);

                ESP_LOGI(
                    "total", "%d ms - total time spent\n", time_download + time_decomp + time_render
                );
            } else {
                printf(
                    "HTTP on finish got status code: %d\n",
                    esp_http_client_get_status_code(evt->client)
                );
            }
            break;

        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED\n");
            break;
    }
    return ESP_OK;
}

// Handles http request
static void http_post(void) {
    /**
     * NOTE: All the configuration parameters for http_client must be specified
     * either in URL or as host and path parameters.
     */
    esp_http_client_config_t config = { .url = IMG_URL,
#if VALIDATE_SSL_CERTIFICATE == true
                                        .cert_pem = (char*)server_cert_pem_start,
#endif
                                        .disable_auto_redirect = false,
                                        .event_handler = _http_event_handler,
                                        .buffer_size = HTTP_RECEIVE_BUFFER_SIZE };

    esp_http_client_handle_t client = esp_http_client_init(&config);

#if DEBUG_VERBOSE
    printf("Free heap before HTTP download: %d\n", xPortGetFreeHeapSize());
    if (esp_http_client_get_transport_type(client) == HTTP_TRANSPORT_OVER_SSL && config.cert_pem) {
        printf("SSL CERT:\n%s\n\n", (char*)server_cert_pem_start);
    }
#endif

    esp_err_t err = esp_http_client_perform(client);
    if (err == ESP_OK) {
        ESP_LOGI(
            TAG,
            "\nIMAGE URL: %s\n\nHTTP GET Status = %d, content_length = %d\n",
            IMG_URL,
            esp_http_client_get_status_code(client),
            esp_http_client_get_content_length(client)
        );
    } else {
        ESP_LOGE(TAG, "\nHTTP GET request failed: %s", esp_err_to_name(err));
    }

    esp_http_client_cleanup(client);

#if MILLIS_DELAY_BEFORE_SLEEP > 0
    vTaskDelay(MILLIS_DELAY_BEFORE_SLEEP / portTICK_PERIOD_MS);
#endif
    printf("Go to sleep %d minutes\n", DEEPSLEEP_MINUTES_AFTER_RENDER);
    epd_poweroff();
    deepsleep();
}

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;

/* The event group allows multiple bits for each event, but we only care about two events:
 * - we are connected to the AP with an IP
 * - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static int s_retry_num = 0;

static void event_handler(
    void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data
) {
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < 5) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "Retry to connect to the AP");
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
            ESP_LOGI(
                TAG,
                "Connect to the AP failed %d times. Going to deepsleep %d minutes",
                5,
                DEEPSLEEP_MINUTES_AFTER_RENDER
            );
            deepsleep();
        }
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

// Initializes WiFi the ESP-IDF way
void wifi_init_sta(void) {
    s_wifi_event_group = xEventGroupCreate();

    ESP_ERROR_CHECK(esp_netif_init());

    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id
    ));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip
    ));
    // C++ wifi config
    wifi_config_t wifi_config;
    memset(&wifi_config, 0, sizeof(wifi_config));
    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
    sprintf(reinterpret_cast<char*>(wifi_config.sta.ssid), ESP_WIFI_SSID);
    sprintf(reinterpret_cast<char*>(wifi_config.sta.password), ESP_WIFI_PASSWORD);
    wifi_config.sta.pmf_cfg.capable = true;

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config((wifi_interface_t)ESP_IF_WIFI_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_LOGI(TAG, "wifi_init_sta finished.");

    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed
     * for the maximum number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see
     * above) */
    EventBits_t bits = xEventGroupWaitBits(
        s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY
    );

    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which
     * event actually happened. */
    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Connected to ap SSID:%s", ESP_WIFI_SSID);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s", ESP_WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }

    /* The event will not be processed after unregister */
    ESP_ERROR_CHECK(
        esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip)
    );
    ESP_ERROR_CHECK(
        esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id)
    );
    vEventGroupDelete(s_wifi_event_group);
}

void app_main() {
    epd_init(EPD_OPTIONS_DEFAULT);
    hl = epd_hl_init(WAVEFORM);
    fb = epd_hl_get_framebuffer(&hl);

    printf("JPGDEC version @bitbank2\n");
    dither_space = (uint8_t*)heap_caps_malloc(epd_width() * 16, MALLOC_CAP_SPIRAM);
    if (dither_space == NULL) {
        ESP_LOGE("main", "Initial alloc ditherSpace failed!");
    }

    // Should be big enough to allocate the JPEG file size, width * height should suffice
    source_buf = (uint8_t*)heap_caps_malloc(epd_width() * epd_height(), MALLOC_CAP_SPIRAM);
    if (source_buf == NULL) {
        ESP_LOGE("main", "Initial alloc source_buf failed!");
    }
    printf("Free heap after buffers allocation: %d\n", xPortGetFreeHeapSize());

    double gammaCorrection = 1.0 / gamma_value;
    for (int gray_value = 0; gray_value < 256; gray_value++)
        gamme_curve[gray_value] = round(255 * pow(gray_value / 255.0, gammaCorrection));

#if JPEG_CPY_FRAMEBUFFER == false
    epd_set_rotation(DISPLAY_ROTATION);
#endif

    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // WiFi log level set only to Error otherwise outputs too much
    esp_log_level_set("wifi", ESP_LOG_ERROR);
    epd_poweron();
    epd_fullclear(&hl, 25);

    // Initialization: WiFi + clean screen while downloading image
    printf("Free heap before wifi_init_sta: %d\n", xPortGetFreeHeapSize());
    wifi_init_sta();
#if VALIDATE_SSL_CERTIFICATE == true
    obtain_time();
#endif

    http_post();
}
