#include <sys/param.h>

#include "esp_log.h"
#include "esp_http_server.h"
#include "esp_ota_ops.h"

#include "http_service.h"

#define MAX_CACHE_SIZE 1024u // HTTP服务接收数据的最大缓存大小

#define MAX_RETRY_COUNT 2 // HTTP服务超时后的最大重试次数

static const char *TAG = "http_service";

httpd_handle_t http_server = NULL;

static void http_resp_favicon_ico(httpd_req_t *req)
{
    extern const unsigned char favicon_ico_start[] asm("_binary_favicon_ico_start");
    extern const unsigned char favicon_ico_end[] asm("_binary_favicon_ico_end");
    const size_t favicon_ico_size = (favicon_ico_end - favicon_ico_start);

    ESP_LOGI(TAG, "---- 下载网站图标");
    httpd_resp_set_type(req, "image/x-icon");
    httpd_resp_send(req, (const char *)favicon_ico_start, favicon_ico_size);
}

static void http_resp_index_html(httpd_req_t *req)
{
    extern const unsigned char index_html_start[] asm("_binary_index_html_start");
    extern const unsigned char index_html_end[] asm("_binary_index_html_end");
    const size_t index_html_size = (index_html_end - index_html_start);
    ESP_LOGI(TAG, "---- 固件升级首页");
    httpd_resp_set_type(req, "text/HTML");
    httpd_resp_send(req, (const char *)index_html_start, index_html_size);
}

static void http_resp_ota_app_html(httpd_req_t *req)
{
    extern const unsigned char ota_app_html_start[] asm("_binary_ota_app_html_start");
    extern const unsigned char ota_app_html_end[] asm("_binary_ota_app_html_end");
    const size_t ota_app_html_size = (ota_app_html_end - ota_app_html_start);

    ESP_LOGI(TAG, "---- get into ota app page");
    httpd_resp_set_type(req, "text/HTML");
    httpd_resp_send(req, (const char *)ota_app_html_start, ota_app_html_size);
}

static esp_err_t http_resp_handler(httpd_req_t *req)
{
    const char *uri_get = req->uri;
    ESP_LOGI(TAG, "---- request uri: http://192.168.4.1%s", req->uri);
    if (strcmp(uri_get, "/") == 0)
    {
        http_resp_ota_app_html(req);
    }
    else if (strcmp(uri_get, "/favicon.ico") == 0)
    {
        http_resp_favicon_ico(req);
    }
    else if (strcmp(uri_get, "/ota/app") == 0)
    {
        http_resp_ota_app_html(req);
    }
    else
    {
        ESP_LOGI(TAG, "---- unknown uri, back to index page");
        http_resp_ota_app_html(req);
    }

    return ESP_OK;
}

static esp_err_t http_ota_app_handler(httpd_req_t *req)
{
    int ret = 0;
    int recv_block = 0;
    int remaining = req->content_len;
    int total = remaining;
    double percent = 0.0;
    int err_timeout_retry_cnt = 0;

    ESP_LOGI(TAG, "---- expect ota data length(bytes): %d", remaining);
    esp_ota_handle_t app_update_handle = 0;
    const esp_partition_t *app_update_partition = esp_ota_get_next_update_partition(NULL);
    esp_err_t err = esp_ota_begin(app_update_partition, OTA_WITH_SEQUENTIAL_WRITES, &app_update_handle);

    if (err == ESP_OK)
    {
        char *http_buffer = (char *)malloc(MAX_CACHE_SIZE);
        while (remaining > 0)
        {
            /* Read the data for the request */
            if ((ret = httpd_req_recv(req, http_buffer, MIN(remaining, MAX_CACHE_SIZE))) <= 0)
            {
                if (ret == HTTPD_SOCK_ERR_TIMEOUT)
                {
                    err_timeout_retry_cnt++;
                    if (err_timeout_retry_cnt >= MAX_RETRY_COUNT)
                    {
                        httpd_resp_send_408(req);
                        ESP_LOGE(TAG, "---- get ota data timeout, stop ota process, system will reboot after one second.");
                        break;
                    }
                    else
                    {
                        /* Retry receiving if timeout occurred */
                        ESP_LOGW(TAG, "---- get ota data timeout, try it again.");
                        continue;
                    }
                }
                else
                {
                    httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file, system will reboot after one second.");
                    ESP_LOGE(TAG, "---- get ota data error, stop ota process, system will reboot after one second.");
                    break;
                }
            }

            err_timeout_retry_cnt = 0;

            err = esp_ota_write(app_update_handle, http_buffer, ret);
            if (err != ESP_OK)
            {
                ESP_LOGI(TAG, "---- write ota data failed, error message:(%s), system will reboot after one second.", esp_err_to_name(err));
                break;
            }
            else
            {
                remaining -= ret;
	            recv_block++;
	            if ((recv_block % 16) == 0)
	            {
	                percent = 100.0 - (double)(remaining * 100) / (double)total;
	                ESP_LOGI(TAG, "---- ota process: %.2f%%", percent);
	            }
            }
        }

        free(http_buffer);

        if (remaining == 0)
        {
            err = esp_ota_end(app_update_handle);
            if (err == ESP_OK)
            {
                esp_err_t err = esp_ota_set_boot_partition(app_update_partition);
                if (err == ESP_OK)
                {
                    if (err == ESP_OK)
                    {
                        ESP_LOGI(TAG, "---- ota process: 100.00%%");
                        ESP_LOGI(TAG, "---- ota success, reboot after one seconds");
                    }
                    else
                    {
                        ESP_LOGE(TAG, "---- ota success, but error occurred when write firmware info into eeprom.");
                    }
                }
                else
                {
                    ESP_LOGE(TAG, "---- set boot partition failed, error message:(%s), system will reboot after one second.", esp_err_to_name(err));
                }
            }
            else
            {
                ESP_LOGE(TAG, "---- verify program fail, error message:(%s), system will reboot after one second.", esp_err_to_name(err));
            }
        }
        else
        {
            ESP_LOGE(TAG, "---- ota data lost, stop ota process, system will reboot after one second.");
        }
    }
    else
    {
        ESP_LOGE(TAG, "---- can not find next app partition");
    }

    httpd_resp_send_chunk(req, NULL, 0);
    vTaskDelay(pdMS_TO_TICKS(1000));
    esp_restart();
    return ESP_OK;
}

httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;
    config.uri_match_fn = httpd_uri_match_wildcard;

    // Start the httpd server
    ESP_LOGI(TAG, "---- httpd server port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK)
    {
        // Set URI handlers
        ESP_LOGI(TAG, "---- Set URI handlers");

        // register client GET handle
        httpd_uri_t http_resp = {
            .uri = "/*",
            .method = HTTP_GET,
            .handler = http_resp_handler,
            .user_ctx = NULL};
        httpd_register_uri_handler(server, &http_resp);

        // register client ota app handle
        httpd_uri_t http_ota_app = {
            .uri = "/ota/app/*",
            .method = HTTP_POST,
            .handler = http_ota_app_handler,
            .user_ctx = NULL};
        httpd_register_uri_handler(server, &http_ota_app);

        return server;
    }

    ESP_LOGI(TAG, "---- error occurred during server startup");
    return NULL;
}

esp_err_t stop_webserver(httpd_handle_t server)
{
    return httpd_stop(server);
}

void start_webserver_handler(void *arg)
{
    httpd_handle_t *server = (httpd_handle_t *)arg;
    if (*server == NULL)
    {
        ESP_LOGI(TAG, "---- start http server");
        *server = start_webserver();
    }
}

void stop_webserver_handler(void *arg)
{
    httpd_handle_t *server = (httpd_handle_t *)arg;
    if (*server)
    {
        ESP_LOGI(TAG, "---- stop http server");
        if (stop_webserver(*server) == ESP_OK)
        {
            *server = NULL;
        }
        else
        {
            ESP_LOGE(TAG, "Failed to stop http server");
        }
    }
}
