#include <stdio.h>
#include <inttypes.h>
#include <locale.h>
#include <time.h>
#include <string.h>
#include <sys/time.h>
#include "esp_spiffs.h"

#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"

#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "driver/gpio.h"

#define PRINT_DEBUG false

#define DHT11_GPIO 0
#define BUTTON_PIN  9  // BOOT button on ESP32-C3 SuperMini

#define DATA_BUFFER_SIZE 100
#define MAX_MEASUREMENTS_PER_DAY 96  // 96 * 15 minutes = 24 hours
static int measurement_count = 0;
QueueHandle_t sensor_data_queue;
SemaphoreHandle_t xMutex;

typedef struct {
    char timestamp[64];
    float humidity;
    float temperature;
} SensorData;

TaskHandle_t file_handler_task_handle;

// ############### Base Setup ###############

void set_system_time_from_compiler() {
    // Parse __DATE__: "Mmm dd yyyy"
    char date[] = __DATE__;
    char time[] = __TIME__;

    struct tm tm = {0};
    char month[4];
    int day, year;
    int hour, min, sec;

    // Parse date
    sscanf(date, "%3s %d %d", month, &day, &year);

    // Parse time
    sscanf(time, "%d:%d:%d", &hour, &min, &sec);

    // Convert month abbreviation to number
    const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
                           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
    for (int i = 0; i < 12; i++) {
        if (strcmp(month, months[i]) == 0) {
            tm.tm_mon = i;
            break;
        }
    }
    tm.tm_mday = day;
    tm.tm_year = year - 1900; // Years since 1900
    tm.tm_hour = hour;
    tm.tm_min = min;
    tm.tm_sec = sec;
    tm.tm_isdst = -1; // Let system determine daylight saving time

    // Set system time
    struct timeval tv = { .tv_sec = mktime(&tm), .tv_usec = 0 };
    settimeofday(&tv, NULL);
}

void print_chip_info(){
    /* Print chip information */
    #if PRINT_DEBUG
    esp_chip_info_t chip_info;
    uint32_t flash_size;
    esp_chip_info(&chip_info);
    printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
           CONFIG_IDF_TARGET,
           chip_info.cores,
           (chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
           (chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
           (chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
           (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");

    unsigned major_rev = chip_info.revision / 100;
    unsigned minor_rev = chip_info.revision % 100;
    printf("silicon revision v%d.%d, ", major_rev, minor_rev);
    if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
        printf("Get flash size failed");
        return;
    }

    printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
           (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");

    printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
    #endif
}

// ############### Measurement task ###############

void get_current_time(char* time_string, size_t size) {
    time_t now;
    struct tm timeinfo;

    time(&now);
    localtime_r(&now, &timeinfo);
    strftime(time_string, size, "%Y-%m-%d %H:%M:%S", &timeinfo);
}

void read_dht11(float *humidity, float *temperature) {
    uint8_t data[5] = {0};

    // Send start signal: pull the line low for 20ms
    gpio_set_direction(DHT11_GPIO, GPIO_MODE_OUTPUT);
    gpio_set_level(DHT11_GPIO, 0);
    vTaskDelay(pdMS_TO_TICKS(20));

    // Release the line and wait for sensor response
    gpio_set_level(DHT11_GPIO, 1);
    gpio_set_direction(DHT11_GPIO, GPIO_MODE_INPUT);

    // Wait for the sensor to pull the line low (~20-40us)
    while (gpio_get_level(DHT11_GPIO) == 1) {
        vTaskDelay(pdMS_TO_TICKS(0.1));
    }

    // Wait for the sensor to pull the line high (~80us)
    while (gpio_get_level(DHT11_GPIO) == 0) {
        vTaskDelay(pdMS_TO_TICKS(0.1));
    }

    // Read 40 bits of data
    for (int i = 0; i < 40; i++) {
        // Wait for the start of the bit (low for ~50us)
        while (gpio_get_level(DHT11_GPIO) == 1) {
            vTaskDelay(pdMS_TO_TICKS(0.1));
        }
        while (gpio_get_level(DHT11_GPIO) == 0) {
            vTaskDelay(pdMS_TO_TICKS(0.1));
        }

        // Measure the length of the high pulse
        uint32_t pulse_start = esp_timer_get_time();
        while (gpio_get_level(DHT11_GPIO) == 1) {
            vTaskDelay(pdMS_TO_TICKS(0.1));
        }
        uint32_t pulse_end = esp_timer_get_time();

        // Determine if the bit is 0 or 1
        uint8_t bit = (pulse_end - pulse_start) > 50 ? 1 : 0;
        data[i / 8] <<= 1;
        data[i / 8] |= bit;
    }

    // Parse the data
    uint8_t humidity_int = data[0];
    uint8_t humidity_dec = data[1]; // Decimal part (0-9)
    int8_t temperature_int = (data[2]); // signed value
    uint8_t temperature_dec = data[3]; // Decimal part (0-9)
    uint8_t checksum = data[4];

    #if PRINT_DEBUG
        printf("\nReadout values: [hi: %d, hd: %d, ti %d, td %d,  c: %d]\n", humidity_int, humidity_dec, temperature_int, temperature_dec, checksum);
    #endif

    // Validate checksum
    uint8_t sum = humidity_int + humidity_dec + temperature_int + temperature_dec;
    if (sum != checksum) {
        #if PRINT_DEBUG 
            printf("Checksum failed! Retrying...\n");
        #endif
        vTaskDelay(pdMS_TO_TICKS(1000));
        read_dht11(humidity, temperature);
    }
    #if PRINT_DEBUG 
        printf("Checksum checks out!\n");
    #endif

    // Calculate humidity and temperature
    *humidity = (float)humidity_int + (float)humidity_dec / 10.0f;
    *temperature = (float)temperature_int + (float)temperature_dec / 10.0f;
}

void measure_task(void *pvParameters){
    while (1) {
        float humidity, temperature;
        read_dht11(&humidity, &temperature);
        
        char time_string[64] = {0};
        get_current_time(time_string, sizeof(time_string));

        #if PRINT_DEBUG 
            printf("Measurment: [%s, %.1f%%,  %.1f°C]\n", time_string, humidity, temperature);
        #endif

        SensorData data;
        strncpy(data.timestamp, time_string, sizeof(data.timestamp));
        data.humidity = humidity;
        data.temperature = temperature;

        // Send data to the queue
        if (xQueueSend(sensor_data_queue, &data, pdMS_TO_TICKS(100)) != pdPASS) {
            #if PRINT_DEBUG
            printf("Failed to add measurement to queue!\n");
            #endif
        } else {
            measurement_count++;
            if (measurement_count >= MAX_MEASUREMENTS_PER_DAY) {
                // Signal the file handler to write and reset
                xTaskNotifyGive(file_handler_task_handle);
            }
        }

        // Wait 15 minute before next read
        vTaskDelay(pdMS_TO_TICKS(15 * 60 * 1000));
    }
}

// ############### Filehandling task ###############

void init_filesystem() {
    esp_vfs_spiffs_conf_t conf = {
        .base_path = "/spiffs",
        .partition_label = "storage",
        .max_files = 5,
        .format_if_mount_failed = true
    };
    esp_err_t ret = esp_vfs_spiffs_register(&conf);
    if (ret != ESP_OK) {
        #if PRINT_DEBUG
        if (ret == ESP_FAIL) {
            printf("Failed to mount or format filesystem\n");
        } else if (ret == ESP_ERR_NOT_FOUND) {
            printf("Failed to find SPIFFS partition\n");
        } else {
            printf("Failed to initialize SPIFFS (%s)\n", esp_err_to_name(ret));
        }
        #endif
        return;
    }
    #if PRINT_DEBUG
    printf("SPIFFS initialized successfully\n");
    #endif
}

void file_handler_task(void *pvParameters) {
    while (1) {
        // Wait for notification from measurement task
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        SensorData data;
        FILE *f = NULL;
    
        xSemaphoreTake(xMutex, portMAX_DELAY);
        f = fopen("/spiffs/measurements.csv", "a");
        if (f == NULL) {
            #if PRINT_DEBUG
            printf("Failed to open CSV file!\n");
            #endif
            xSemaphoreGive(xMutex);
            continue;
        }

        // Write header if file is empty
        fseek(f, 0, SEEK_END);
        if (ftell(f) == 0) {
            fprintf(f, "Timestamp,Humidity,Temperature\n");
        }

        // Read all data from the queue and write to CSV
        while (xQueueReceive(sensor_data_queue, &data, 0) == pdPASS) {
            fprintf(f, "%s,%.1f,%.1f\n", data.timestamp, data.humidity, data.temperature);
            #if PRINT_DEBUG
            printf("%s,%.1f,%.1f\n", data.timestamp, data.humidity, data.temperature);
            #endif
        }

        fclose(f);
        measurement_count = 0;
        #if PRINT_DEBUG
        printf("CSV file updated!\n");
        #endif
        xSemaphoreGive(xMutex);
    }
}

// ############### Button task ###############

void button_task(void *pvParameters) {
    // Configure BOOT button (GPIO9, active low with internal pullup)
    gpio_set_direction(BUTTON_PIN, GPIO_MODE_INPUT);
    gpio_set_pull_mode(BUTTON_PIN, GPIO_PULLUP_ONLY);

    uint32_t last_press_tick = 0;
    bool waiting_for_double = false;

    while (1) {
        if (gpio_get_level(BUTTON_PIN) == 0) { // Button pressed (active low)
            vTaskDelay(pdMS_TO_TICKS(50)); // Debounce
            if (gpio_get_level(BUTTON_PIN) == 0) {
                uint32_t press_start = xTaskGetTickCount();

                // Wait for release
                while (gpio_get_level(BUTTON_PIN) == 0) {
                    vTaskDelay(pdMS_TO_TICKS(10));
                }
                vTaskDelay(pdMS_TO_TICKS(50)); // Debounce release
                uint32_t press_duration = xTaskGetTickCount() - press_start;

                // Only count short presses (< 500ms)
                if (press_duration < pdMS_TO_TICKS(500)) {
                    if (waiting_for_double) {
                        // ===== DOUBLE PRESS: Reset CSV =====
                        xSemaphoreTake(xMutex, portMAX_DELAY);
                        FILE *f = fopen("/spiffs/measurements.csv", "w");
                        if (f) {
                            fprintf(f, "Timestamp,Humidity,Temperature\n");
                            fclose(f);
                        }
                        xSemaphoreGive(xMutex);
                        waiting_for_double = false;
                    } else {
                        // ===== FIRST PRESS: Wait for possible double press =====
                        waiting_for_double = true;
                        last_press_tick = press_start;
                    }
                }
            }
            } else {
            // Check if double-press window expired
            if (waiting_for_double && (xTaskGetTickCount() - last_press_tick) > pdMS_TO_TICKS(500)) {
                xTaskNotifyGive(file_handler_task_handle);
                // ===== SINGLE PRESS: Print CSV contents (via USB CDC) =====
                xSemaphoreTake(xMutex, portMAX_DELAY);
                FILE *f = fopen("/spiffs/measurements.csv", "r");
                if (f) {
                    char line[256];
                    while (fgets(line, sizeof(line), f)) {
                        printf("%s", line); // Goes to USB CDC (/dev/ttyACM0)
                    }
                    fclose(f);
                }
                xSemaphoreGive(xMutex);
                waiting_for_double = false;
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// ############### Shared buffer ###############

void init_shared_buffer() {
    sensor_data_queue = xQueueCreate(DATA_BUFFER_SIZE, sizeof(SensorData));
    xMutex = xSemaphoreCreateMutex();
    if (sensor_data_queue == NULL || xMutex == NULL) {
        #if PRINT_DEBUG
        printf("Failed to create queue or mutex!\n");
        #endif
    }
}

// ############### Main thread ###############

void app_main(void)
{
    // Setup
    setlocale(LC_ALL, "C");  // Use ASCII for text
    set_system_time_from_compiler();
    init_shared_buffer();
    init_filesystem();

    // Intro
    #if PRINT_DEBUG
    printf("DHT11 temperature and humidity sensor\n");
    char start_time[64] = {0};
    get_current_time(start_time, sizeof(start_time));
    printf("Program started at: %s\n", start_time);
    print_chip_info();
    printf("\n\n=============\n\n");
    #endif
    
    // Run tasks
    xTaskCreate(measure_task, "MeasureTask", 4096, NULL, 3, NULL);
    xTaskCreate(file_handler_task, "FileHandlerTask", 4096, NULL, 4, &file_handler_task_handle);
    xTaskCreate(button_task, "ButtonTask", 2048, NULL, 2, NULL);
}
