/*
 * This program source code file is part of the KTag project, a DIY laser tag
 * game with customizable features and wide interoperability.
 *
 * 🛡 <https://ktag.clubk.club> 🃞
 *
 * Copyright © 2024-2026 Joseph P. Kearney and the KTag developers.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 * details.
 *
 * There should be a copy of the GNU Affero General Public License in the LICENSE
 * file in the root of this repository.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <SystemK.h>
#include <System_Events.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <driver/i2s_std.h>
#include <esp_spiffs.h>
#include <audio_player.h>

// I2S Configuration
#define I2S_DOUT GPIO_NUM_11
#define I2S_BCLK GPIO_NUM_12
#define I2S_LRC GPIO_NUM_13
#define I2S_DIN I2S_GPIO_UNUSED
#define I2S_SCLK I2S_GPIO_UNUSED

#define AUDIO_TASK_STACK_SIZE 4096

static StackType_t xStack[AUDIO_TASK_STACK_SIZE];
static StaticTask_t xTaskBuffer;
static QueueHandle_t xQueueAudio;
static TaskHandle_t xAudioTaskHandle;
void I2SAudioTask(void *pvParameters);

static const char *TAG = "I2S Audio";
static i2s_chan_handle_t tx_handle;

#define I2S_GPIO_CFG           \
    {                          \
        .mclk = I2S_SCLK,      \
        .bclk = I2S_BCLK,      \
        .ws = I2S_LRC,         \
        .dout = I2S_DOUT,      \
        .din = I2S_DIN,        \
        .invert_flags = {      \
            .mclk_inv = false, \
            .bclk_inv = false, \
            .ws_inv = false,   \
        },                     \
    }

#define I2S_DUPLEX_STEREO_CFG(_sample_rate)                                                             \
    {                                                                                                   \
        .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sample_rate),                                            \
        .slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), \
        .gpio_cfg = I2S_GPIO_CFG,                                                                       \
    }

static esp_err_t I2S_Audio_Set_Mute(AUDIO_PLAYER_MUTE_SETTING setting)
{
    if (setting == AUDIO_PLAYER_MUTE)
    {
        KLOG_DEBUG(TAG, "Muted.");
    }
    else
    {
        KLOG_DEBUG(TAG, "Muted.");
    }
    return ESP_OK;
}
static esp_err_t I2S_Audio_Write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
    esp_err_t ret = ESP_OK;
    ret = i2s_channel_write(tx_handle, (char *)audio_buffer, len, bytes_written, timeout_ms);
    return ret;
}

static esp_err_t I2S_Audio_Reconfigure_Clock(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
    KLOG_DEBUG(TAG, "Reconfiguring clock for %lu sps, %lu bps, %d channels.", rate, bits_cfg, ch);

    esp_err_t ret = ESP_OK;
    i2s_std_config_t std_cfg = {
        .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate),
        .slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bits_cfg, (i2s_slot_mode_t)ch),
        .gpio_cfg = I2S_GPIO_CFG};

    ret |= i2s_channel_disable(tx_handle);
    ret |= i2s_channel_reconfig_std_clock(tx_handle, &std_cfg.clk_cfg);
    ret |= i2s_channel_reconfig_std_slot(tx_handle, &std_cfg.slot_cfg);
    ret |= i2s_channel_enable(tx_handle);
    return ret;
}

void Initialize_Audio(void)
{
    KLOG_INFO(TAG, "Initializing I2S Audio...");

    // Setup a standard configuration and the channel.
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
    chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer.
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));

    // Setup the I2S configuration.
    i2s_std_config_t std_cfg = I2S_DUPLEX_STEREO_CFG(44100);
    esp_err_t ret = i2s_channel_init_std_mode(tx_handle, &std_cfg);

    if (ret != ESP_OK)
    {
        if (ret == ESP_ERR_NO_MEM)
        {
            KLOG_ERROR(TAG, "No memory for storing the channel information");
        }
        else if (ret == ESP_ERR_INVALID_ARG)
        {
            KLOG_ERROR(TAG, "NULL pointer or invalid configuration");
        }
        else if (ret == ESP_ERR_INVALID_STATE)
        {
            KLOG_ERROR(TAG, "This channel is not registered");
        }
        else
        {
            KLOG_ERROR(TAG, "Failed to initialize I2S Audio (%s)", esp_err_to_name(ret));
        }
    }

    i2s_channel_enable(tx_handle);

    xQueueAudio = xQueueCreate(5, sizeof(AudioAction_T));
    if (xQueueAudio == NULL)
    {
        KLOG_ERROR(TAG, "Couldn't create the I2S Audio queue.");
    }

    audio_player_config_t config = {.mute_fn = I2S_Audio_Set_Mute,
                                    .write_fn = I2S_Audio_Write,
                                    .clk_set_fn = I2S_Audio_Reconfigure_Clock,
                                    .priority = 10,
                                    .coreID = APP_CPU_NUM};
    ret = audio_player_new(config);
    if (ret != ESP_OK)
    {
        KLOG_ERROR(TAG, "Couldn't create the audio player.");
    }

    xAudioTaskHandle = xTaskCreateStaticPinnedToCore(
        I2SAudioTask,          // Function that implements the task.
        "I2S Audio",           // Text name for the task.
        AUDIO_TASK_STACK_SIZE, // Stack size in words, not bytes.
        NULL,                  // Parameter passed into the task.
        1,                     // Priority at which the task is created.
        xStack,                // Array to use as the task's stack.
        &xTaskBuffer,          // Variable to hold the task's data structure.
        APP_CPU_NUM);          // Core where the task should run.

    if (xAudioTaskHandle == NULL)
    {
        KLOG_ERROR(TAG, "Couldn't create the I2S Audio task.");
    }

     xEventGroupSetBits(Get_System_Events(), SYS_AUDIO_READY);
     KLOG_INFO(TAG, "Initialization complete.");
}

esp_err_t Play_Audio_File(const char *filename)
{
    KLOG_INFO(TAG, "Attempting to play file: %s", filename);
    KLOG_DEBUG(TAG, "Free heap: %ld bytes", esp_get_free_heap_size());

    FILE *fh = fopen(filename, "rb");
    if (fh == NULL)
    {
        KLOG_ERROR(TAG, "Failed to open audio file %s! Error: %s", filename, strerror(errno));
        return ESP_ERR_NOT_FOUND;
    }

    fseek(fh, 0, SEEK_END);
    long file_size = ftell(fh);
    fseek(fh, 0, SEEK_SET);
    KLOG_DEBUG(TAG, "File size: %ld bytes", file_size);

    esp_err_t ret = audio_player_play(fh);
    if (ret != ESP_OK)
    {
        KLOG_ERROR(TAG, "Couldn't play audio file %s! Error: %d", filename, ret);
        fclose(fh);
        return ret;
    }

    return ESP_OK;
}

SystemKResult_T Perform_Audio_Action(AudioAction_T *action)
{
    if (xQueueSend(xQueueAudio, action, 0) == pdTRUE)
    {
        return SYSTEMK_RESULT_SUCCESS;
    }
    else
    {
        return SYSTEMK_RESULT_QUEUE_IS_FULL;
    }
}

char *concat_path(const char *directory_path, const char *filename)
{
    size_t needed = snprintf(NULL, 0, "%s/%s", directory_path, filename) + 1;
    char *full_path = malloc(needed);
    if (full_path != NULL)
    {
        snprintf(full_path, needed, "%s/%s", directory_path, filename);
    }
    return full_path;
}

char *find_filename(const char *directory_path, const char *prefix)
{
    DIR *dir = opendir(directory_path);
    if (dir == NULL)
    {
        KLOG_ERROR(TAG, "Failed to open directory '%s': %s!", directory_path, strerror(errno));
        return NULL;
    }

    struct dirent *entry;
    char *filename = NULL;

    while ((entry = readdir(dir)) != NULL)
    {
        if (entry->d_type == DT_REG && strncmp(entry->d_name, prefix, strlen(prefix)) == 0)
        {
            filename = strdup(entry->d_name);
            if (filename == NULL)
            {
                KLOG_ERROR(TAG, "Memory allocation failed!");
            }
            else
            {
                KLOG_DEBUG(TAG, "Found matching file: %s", filename);
            }
            break;
        }
    }

    closedir(dir);

    if (filename == NULL)
    {
        KLOG_WARN(TAG, "No file beginning with '%s' found in directory '%s'!", prefix, directory_path);
    }

    return filename;
}

SystemKResult_T Play_Sound_By_Prefix(const char *prefix)
{
    SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;

    // Check for USB audio files.
    EventBits_t bits = xEventGroupWaitBits(
        Get_System_Events(),
        SYS_USB_FS_PRESENT,
        pdFALSE,
        pdTRUE,
        0);

    if ((bits & SYS_USB_FS_PRESENT) == 0)
    {
        KLOG_ERROR(TAG, "USB file system not present!");
        result = SYSTEMK_RESULT_FILESYSTEM_NOT_PRESENT;
    }
    else
    {
        const char *sounds_dir = "/usb/01";

        char *filename = find_filename(sounds_dir, prefix);

        if (filename != NULL)
        {
            if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
            {
                audio_player_stop();
            }
            Play_Audio_File(concat_path(sounds_dir, filename));
        }
        else
        {
            result = SYSTEMK_RESULT_FILE_NOT_FOUND;
        }
    }

    return result;
}

SystemKResult_T Play_Number_Sound(uint8_t number)
{
    SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;

    char number_str[4];
    snprintf(number_str, 4, "%03u", number);

    const char *sounds_dir = "/usb/10";

    char *filename = find_filename(sounds_dir, (const char *)number_str);

    if (filename != NULL)
    {
        if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
        {
            audio_player_stop();
        }
        Play_Audio_File(concat_path(sounds_dir, filename));
    }
    else
    {
        result = SYSTEMK_RESULT_FILE_NOT_FOUND;
    }

    return result;
}

void I2SAudioTask(void *pvParameters)
{
    // Wait for system initialization.
    // TODO: Make this closed-loop!
    vTaskDelay(1000 / portTICK_PERIOD_MS);

    while (true)
    {
        AudioAction_T action;

        if (xQueueReceive(xQueueAudio, &action, portMAX_DELAY) == pdPASS)
        {
            SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;

            switch (action.ID)
            {
            case AUDIO_SET_VOLUME:
                break;

            case AUDIO_SILENCE:
                if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
                {
                    audio_player_stop();
                }
                break;

            case AUDIO_PLAY_STARTUP_SOUND:
                result = Play_Sound_By_Prefix("001");
                break;

            case AUDIO_PLAY_SHOT_FIRED:
                result = Play_Sound_By_Prefix("002");
                break;

            case AUDIO_PLAY_TAG_RECEIVED:
                result = Play_Sound_By_Prefix("003");
                break;

            case AUDIO_PLAY_TAGGED_OUT:
                result = Play_Sound_By_Prefix("004");
                break;

            case AUDIO_PLAY_MISFIRE:
                result = Play_Sound_By_Prefix("005");
                break;

            case AUDIO_PRONOUNCE_NUMBER_0_TO_100:
                uint8_t number = *((uint8_t *)action.Data);
                if (number > 100)
                {
                    number = 100;
                }
                else if (number == 0)
                {
                    number = 101;
                }
                result = Play_Number_Sound(number);
                break;

            case AUDIO_PLAY_MENU_PROMPT:
                result = Play_Sound_By_Prefix("006");
                break;

            case AUDIO_PLAY_SELECTION_INDICATOR:
                result = Play_Sound_By_Prefix("007");
                break;

            case AUDIO_PLAY_HEALTH_REMAINING:
                result = Play_Sound_By_Prefix("008");
                break;

            case AUDIO_PLAY_ELECTRONIC_DANCE_MUSIC:
                result = Play_Sound_By_Prefix("009");
                break;

            case AUDIO_PLAY_GENERIC_ERROR:
                result = Play_Sound_By_Prefix("010");
                break;

            case AUDIO_PLAY_VOLUME_PROMPT:
                result = Play_Sound_By_Prefix("011");
                break;

            case AUDIO_PLAY_RIGHT_HANDED:
                result = Play_Sound_By_Prefix("012");
                break;

            case AUDIO_PLAY_LEFT_HANDED:
                result = Play_Sound_By_Prefix("013");
                break;

            case AUDIO_PLAY_GAME_ON:
                result = Play_Sound_By_Prefix("014");
                break;

            case AUDIO_PLAY_HARDWARE_SETTINGS_PROMPT:
                result = Play_Sound_By_Prefix("015");
                break;

            case AUDIO_PLAY_GAME_SETTINGS_PROMPT:
                result = Play_Sound_By_Prefix("016");
                break;

            case AUDIO_PLAY_BONK:
                result = Play_Sound_By_Prefix("017");
                break;

            case AUDIO_PLAY_NEAR_MISS:
                result = Play_Sound_By_Prefix("018");
                break;

            case AUDIO_PLAY_PLAYER_ID_PROMPT:
                result = Play_Sound_By_Prefix("019");
                break;

            case AUDIO_PLAY_TEAM_ID_PROMPT:
                result = Play_Sound_By_Prefix("020");
                break;

            case AUDIO_PLAY_FRIENDLY_FIRE:
                KLOG_WARN(TAG, "\"Friendly Fire\" audio is disabled in this build.");
                // result = Play_Sound_By_Prefix("021");
                break;

            case AUDIO_PLAY_STARTING_THEME:
                result = Play_Sound_By_Prefix("022");
                break;

            case AUDIO_PLAY_BOOP:
                result = Play_Sound_By_Prefix("023");
                break;

            case AUDIO_PLAY_BEEP:
                result = Play_Sound_By_Prefix("024");
                break;

            case AUDIO_PLAY_REPROGRAMMING:
                result = Play_Sound_By_Prefix("025");
                break;

            case AUDIO_PLAY_BOMB:
                result = Play_Sound_By_Prefix("026");
                break;

            case AUDIO_PLAY_GAME_OVER:
                result = Play_Sound_By_Prefix("027");
                break;

            default:
                Play_Audio_File("/spiffs/KTag_broken.mp3");
                break;
            }

            if (result == SYSTEMK_RESULT_FILESYSTEM_NOT_PRESENT)
            {
                result = Play_Audio_File("/spiffs/KTag_broken.mp3");
            }
            
            if (result == SYSTEMK_RESULT_SUCCESS)
            {
                if (action.Play_To_Completion == true)
                {
                    // Allow some time for the audio to start.
                    vTaskDelay(100 / portTICK_PERIOD_MS);

                    while (audio_player_get_state() != AUDIO_PLAYER_STATE_IDLE)
                    {
                        vTaskDelay(100 / portTICK_PERIOD_MS);
                    }

                    KEvent_T command_received_event = {.ID = KEVENT_AUDIO_COMPLETED, .Data = (void *)action.ID};
                    Post_KEvent(&command_received_event);
                }
            }
        }
    }
}
