/* http client request example code

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.

   Demo video: https://www.bilibili.com/video/BV1ekRAYVEZ1/
   Hardware: https://oshwhub.com/esp-college/esp-spot
*/

#include <stdio.h>
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/idf_additions.h"
#include "freertos/task.h"

#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_wifi.h"

#include "audio_sys.h"
#include "audio_thread.h"
#include "esp_peripherals.h"
#include "periph_wifi.h"
#include "periph_sdcard.h"
#include "audio_mem.h"
#include "board.h"
#include "es8311.h"
#include "es7210.h"

#include "touch_button.h"
#include "iot_button.h"
#include "touch_sensor_lowlevel.h"

#include "audio_processor.h"
#include "coze_chat.h"
#include "led_driver.h"

#define DEFAULT_RAW_OPUS_BUFFER_SIZE (1024)

static char *TAG = "main";

#define TOUCH_CHANNEL_1        (3)
#define TOUCH_CHANNEL_2        (9)
#define TOUCH_CHANNEL_3        (13)
#define TOUCH_CHANNEL_4        (14)

#define LIGHT_TOUCH_THRESHOLD  (0.15)
#define HEAVY_TOUCH_THRESHOLD  (0.4)

/* To controll the audio event */
#define BIT_RECORDING_START (1 << 0)
static EventGroupHandle_t s_audio_event_group;

struct coze_ws_s {
    coze_chat_handle_t      chat;
    audio_recorder_handle_t recorder;
    audio_player_handle_t   player;
    char                   *recorder_buffer;
    char                   *opus_raw_buffer;
    int                     opus_raw_buffer_len;
    enum {
        PLAYBACK_STATE_IDLE,
        PLAYBACK_STATE_PLAYING,
    } player_state;
};

static struct coze_ws_s s_coze_ws;
static audio_board_handle_t board_handle = NULL;

static void audio_event_callback(coze_chat_event_t event, void *ctx)
{
    if (event == COZE_CHAT_EVENT_CHAT_SPEECH_STARTED) {
        ESP_LOGI(TAG, "chat start");
        s_coze_ws.player_state = PLAYBACK_STATE_IDLE;
    } else if (event == COZE_CHAT_EVENT_CHAT_SPEECH_STOPED) {
        ESP_LOGI(TAG, "chat stop");
        s_coze_ws.player_state = PLAYBACK_STATE_PLAYING;
    }
}

static void audio_data_callback(char *data, int len, void *ctx)
{
#define frame_length_prefix (2)
    if (len > s_coze_ws.opus_raw_buffer_len) {
        s_coze_ws.opus_raw_buffer_len = len + frame_length_prefix;
        s_coze_ws.opus_raw_buffer = audio_realloc(s_coze_ws.opus_raw_buffer, s_coze_ws.opus_raw_buffer_len);
    }
    ESP_LOGD(TAG, "data: %p, len: %d", data, len);
    s_coze_ws.opus_raw_buffer[0] = (len >> 8) & 0xFF;
    s_coze_ws.opus_raw_buffer[1] = len & 0xFF;
    memcpy(s_coze_ws.opus_raw_buffer + frame_length_prefix, data, len);
    len += frame_length_prefix;

    player_pipeline_write(s_coze_ws.player, s_coze_ws.opus_raw_buffer, len);
}

static void audio_if_open()
{
    s_coze_ws.recorder = recorder_pipeline_open();
    s_coze_ws.player = player_pipeline_open();
    recorder_pipeline_run(s_coze_ws.recorder);
    player_pipeline_run();
}

/**
 * @brief Temporarily mute the audio output to prevent pop or click noise 
 *        during audio state transitions.
 * 
 * This function briefly mutes the audio hardware for a few milliseconds 
 * and then unmutes it. It's typically used before starting or stopping 
 * playback to suppress unwanted transition noise.
 */
static void short_mute()
{
    audio_hal_set_mute(board_handle->audio_hal, true);
    vTaskDelay(pdMS_TO_TICKS(30));
    audio_hal_set_mute(board_handle->audio_hal, false);
}

static void audio_data_read_task(void *pv)
{
#define recorder_buffer_size (640)
    s_coze_ws.recorder_buffer = malloc(recorder_buffer_size);

    s_audio_event_group = xEventGroupCreate();
    xEventGroupSetBits(s_audio_event_group, BIT_RECORDING_START);
    while (1) {
        /* Stop reading audio if the WebSocket stopped */
        xEventGroupWaitBits(s_audio_event_group,
                            BIT_RECORDING_START,
                            pdFALSE,
                            pdTRUE,
                            portMAX_DELAY);
        int r_len = recorder_pipeline_read(s_coze_ws.recorder, s_coze_ws.recorder_buffer, recorder_buffer_size);
        if (r_len > 0) {
            coze_chat_send_audio_data(s_coze_ws.chat, s_coze_ws.recorder_buffer, r_len);
        }
    }
    vTaskDelete(NULL);
}

static void audio_tone_player_event_cb(audio_element_status_t evt)
{
    if (evt == AEL_STATUS_STATE_FINISHED) {
        // add more functions here
    }
}

static void touch_event_nose(void *arg, void *data)
{
    button_event_t event = iot_button_get_event(arg);
    ESP_LOGI(TAG, "touch event nose: %s", iot_button_get_event_str(event));

    // Stop coze or local audio
    player_pipeline_stop();
    audio_tone_stop();

    audio_tone_play(TONE_TYPE_TOUCH_NOSE);
}

static void touch_event_nose_long(void *arg, void *data)
{
    button_event_t event = iot_button_get_event(arg);
    ESP_LOGI(TAG, "touch event nose long: %s", iot_button_get_event_str(event));

    // Stop coze or local audio
    player_pipeline_stop();
    audio_tone_stop();

    audio_tone_play(TONE_TYPE_SCREAMING);
}

static void touch_event_hat(void *arg, void *data)
{
    button_event_t event = iot_button_get_event(arg);
    ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event));

    // Stop coze or local audio
    player_pipeline_stop();
    audio_tone_stop();

    static tone_type_t hat_state = TONE_TYPE_HAT_1;
    audio_tone_play(hat_state);

     // Toggle between two tone states and update LED mode
    if (hat_state == TONE_TYPE_CAPYBARA_SONG_1) {
        // Turn on LED
        led_set_mode(4);
        hat_state = TONE_TYPE_HAT_1;
    } else {
        hat_state = TONE_TYPE_CAPYBARA_SONG_1;
    }
    
}

static void touch_event_belly(void *arg, void *data)
{
    button_event_t event = iot_button_get_event(arg);
    ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event));

    // Stop coze or local audio
    player_pipeline_stop();
    audio_tone_stop();

    static int audio_belly_count_g = 0;
    audio_tone_play(audio_belly_count_g);

    // Update the counter for next playback
    if (audio_belly_count_g < TONE_TYPE_BELLY_4) {
        audio_belly_count_g++;
    } else if (audio_belly_count_g == TONE_TYPE_BELLY_4) {
        audio_belly_count_g = TONE_TYPE_SCREAMING;
    } else {
        audio_belly_count_g = TONE_TYPE_BELLY_1;
    }
}

static void touch_event_neck(void *arg, void *data)
{
    button_event_t event = iot_button_get_event(arg);
    ESP_LOGI(TAG, "touch event hat: %s", iot_button_get_event_str(event));

    // Stop coze or local audio
    player_pipeline_stop();
    audio_tone_stop();

    static tone_type_t neck_state = TONE_TYPE_NECK_2;

     // Toggle between two tone states and update LED mode
    if (neck_state == TONE_TYPE_NECK_2) {
        // Turn on LED
        xEventGroupClearBits(s_audio_event_group, BIT_RECORDING_START);
        recorder_pipeline_stop(s_coze_ws.recorder);
        coze_chat_stop(s_coze_ws.chat);
        audio_tone_play(TONE_TYPE_NECK_2);
        neck_state = TONE_TYPE_NECK_1;
    } else {
        audio_tone_play(TONE_TYPE_NECK_1);
        neck_state = TONE_TYPE_NECK_2;
        coze_chat_start(s_coze_ws.chat);
        xEventGroupSetBits(s_audio_event_group, BIT_RECORDING_START);
        recorder_pipeline_run(s_coze_ws.recorder);
    }
}

static void touch_task(void *arg)
{
    // Register all touch channel
    uint32_t touch_channel_list[] = {TOUCH_CHANNEL_1, TOUCH_CHANNEL_2, TOUCH_CHANNEL_3, TOUCH_CHANNEL_4};
    int total_channel_num = sizeof(touch_channel_list) / sizeof(touch_channel_list[0]);

    // calloc channel_type for every button from the list
    touch_lowlevel_type_t *channel_type = calloc(total_channel_num, sizeof(touch_lowlevel_type_t));
    assert(channel_type);
    for (int i = 0; i  < total_channel_num; i++) {
        channel_type[i] = TOUCH_LOWLEVEL_TYPE_TOUCH;
    }

    touch_lowlevel_config_t low_config = {
        .channel_num = total_channel_num,
        .channel_list = touch_channel_list,
        .channel_type = channel_type,
    };
    esp_err_t ret = touch_sensor_lowlevel_create(&low_config);
    assert(ret == ESP_OK);
    free(channel_type);

    const button_config_t btn_cfg = {
        .short_press_time = 300,
        .long_press_time = 2000,
    };

    /* ============================= Init touch IO3 ============================= */ 
    button_touch_config_t touch_cfg_1 = {
        .touch_channel = touch_channel_list[0],
        .channel_threshold = LIGHT_TOUCH_THRESHOLD,
        .skip_lowlevel_init = true,
    };

    /* Create button for nose */
    button_handle_t btn_nose = NULL;
    ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_1, &btn_nose);
    assert(ret == ESP_OK);

    /* ============================= Init touch IO9 ============================= */ 
    button_touch_config_t touch_cfg_2 = {
        .touch_channel = touch_channel_list[1],
        .channel_threshold = LIGHT_TOUCH_THRESHOLD,
        .skip_lowlevel_init = true,
    };

    /* Create button for hat */
    button_handle_t btn_hat = NULL;
    ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_2, &btn_hat);
    assert(ret == ESP_OK);

    /* ============================= Init touch IO13 ============================= */ 
    button_touch_config_t touch_cfg_3 = {
        .touch_channel = touch_channel_list[2],
        .channel_threshold = LIGHT_TOUCH_THRESHOLD,
        .skip_lowlevel_init = true,
    };

    /* Create light press button */
    button_handle_t btn_belly = NULL;
    ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_3, &btn_belly);
    assert(ret == ESP_OK);

    /* ============================= Init touch IO14 ============================= */ 
    button_touch_config_t touch_cfg_4 = {
        .touch_channel = touch_channel_list[3],
        .channel_threshold = LIGHT_TOUCH_THRESHOLD,
        .skip_lowlevel_init = true,
    };

    /* Create button for nose */
    button_handle_t btn_neck = NULL;
    ret = iot_button_new_touch_button_device(&btn_cfg, &touch_cfg_4, &btn_neck);
    assert(ret == ESP_OK);

    /* ========================== Register touch callback ========================== */ 
    // Register touch callback for nose
    iot_button_register_cb(btn_nose, BUTTON_PRESS_DOWN, NULL, touch_event_nose, NULL);
    iot_button_register_cb(btn_nose, BUTTON_LONG_PRESS_START, NULL, touch_event_nose_long, NULL);

    // Register touch callback for hat
    iot_button_register_cb(btn_hat, BUTTON_PRESS_DOWN, NULL, touch_event_hat, NULL);

    // Register touch callback for belly
    iot_button_register_cb(btn_belly, BUTTON_PRESS_DOWN, NULL, touch_event_belly, NULL);

    // Register touch callback for nose
    iot_button_register_cb(btn_neck, BUTTON_PRESS_DOWN, NULL, touch_event_neck, NULL);

    touch_sensor_lowlevel_start();

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

static void led_task(void *arg)
{
    /* Configure the LED strip and obtain a handle */
    led_strip_handle_t led_strip = led_create();
    led_set_mode(1);
    vTaskDelay(1000);
    led_set_mode(0);

    while(1) {
        /* Run the LED animation based on LED configuration and weather data */
        ESP_ERROR_CHECK(led_animations_start(led_strip));
    }

    vTaskDelete(NULL);
}

void app_main(void)
{
    AUDIO_MEM_SHOW(TAG);
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());

    ESP_LOGI(TAG, "Initialize board peripherals");
    esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
    esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);

    board_handle = audio_board_init();
    audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START);
#if CONFIG_ESP32_S3_SPOT_BOARD
    audio_hal_set_volume(board_handle->audio_hal, 90);
    es8311_set_mic_gain(ES8311_MIC_GAIN_0DB);
#endif

    s_coze_ws.opus_raw_buffer_len = DEFAULT_RAW_OPUS_BUFFER_SIZE;
    s_coze_ws.opus_raw_buffer = audio_malloc(s_coze_ws.opus_raw_buffer_len);
    // Initialize SD Card peripheral
    // audio_board_sdcard_init(set, SD_MODE_1_LINE);
    periph_wifi_cfg_t wifi_cfg = {
        .wifi_config.sta.ssid = CONFIG_ESP_WIFI_SSID,
        .wifi_config.sta.password = CONFIG_ESP_WIFI_PASSWORD,
    };
    esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg);
    esp_periph_start(set, wifi_handle);
    periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY);

    coze_chat_config_t chat_config = COZE_CHAT_DEFAULT_CONFIG();
    chat_config.bot_id = CONFIG_BOT_ID;
    chat_config.access_token = CONFIG_ACCESS_TOKEN;
    chat_config.audio_callback = audio_data_callback;
    chat_config.event_callback = audio_event_callback;

    s_coze_ws.chat = coze_chat_init(&chat_config);
    coze_chat_start(s_coze_ws.chat);

    audio_if_open();
    audio_thread_create(NULL, "audio_data_read_task", audio_data_read_task, (void *)NULL, 1024 * 4, 12, true, 1);

    xTaskCreate(touch_task, "touch_task", 1024 * 5, NULL, 5, NULL);
    xTaskCreate(led_task, "led_task", 1024 * 3, NULL, 5, NULL);

    audio_tone_init(audio_tone_player_event_cb);

    AUDIO_MEM_SHOW(TAG);
}
