/***************************************************************************
 *   Copyright (C) 2022 - 2023 by Federico Amedeo Izzo IU2NUO,             *
 *                                Niccolò Izzo IU2KIN,                     *
 *                                Silvano Seva IU2KWO                      *
 *                                Joseph Stephen VK7JS                     *
 *                                Roger Clark  VK3KYY                      *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU 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 General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, see <http://www.gnu.org/licenses/>   *
 ***************************************************************************/
#include <interfaces/platform.h>
#include <interfaces/keyboard.h>
#include <interfaces/delays.h>
#include <voicePromptUtils.h>
#include <ui/ui_strings.h>
#include <voicePrompts.h>
#include <audio_codec.h>
#include <audio_path.h>
#include <strings.h>    // For strncasecmp
#include <ctype.h>
#include <state.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <beeps.h>
#include <errno.h>

static const uint32_t VOICE_PROMPTS_DATA_MAGIC   = 0x5056;  //'VP'
static const uint32_t VOICE_PROMPTS_DATA_VERSION = 0x1000;  // v1000 OpenRTX

#define VOICE_PROMPTS_TOC_SIZE 350
#define CODEC2_HEADER_SIZE     7
#define VP_SEQUENCE_BUF_SIZE   128
#define BEEP_SEQ_BUF_SIZE      256

typedef struct
{
    uint32_t magic;
    uint32_t version;
}
vpHeader_t;

typedef struct
{
    const char* userWord;
    const voicePrompt_t vp;
}
userDictEntry_t;

typedef struct
{
    uint16_t buffer[VP_SEQUENCE_BUF_SIZE];  // Buffer of individual prompt indices.
    uint16_t pos;                           // Index into above buffer.
    uint16_t length;                        // Number of entries in above buffer.
    uint32_t c2DataStart;
    uint32_t c2DataIndex;                   // Index into current codec2 data
    uint32_t c2DataLength;                  // Length of codec2 data for current prompt.
}
vpSequence_t;

typedef struct
{
    uint16_t freq;
    uint16_t duration;
}
beepData_t;


static const userDictEntry_t userDictionary[] =
{
    {"hotspot",   PROMPT_CUSTOM1},  // Hotspot
    {"clearnode", PROMPT_CUSTOM2},  // ClearNode
    {"sharinode", PROMPT_CUSTOM3},  // ShariNode
    {"microhub",  PROMPT_CUSTOM4},  // MicroHub
    {"openspot",  PROMPT_CUSTOM5},  // Openspot
    {"repeater",  PROMPT_CUSTOM6},  // repeater
    {"blindhams", PROMPT_CUSTOM7},  // BlindHams
    {"allstar",   PROMPT_CUSTOM8},  // Allstar
    {"parrot",    PROMPT_CUSTOM9},  // Parrot
    {"channel",   PROMPT_CHANNEL},  // Channel
    {0, 0}
};

static vpSequence_t vpCurrentSequence =
{
    .pos          = 0,
    .length       = 0,
    .c2DataStart  = 0,
    .c2DataIndex  = 0,
    .c2DataLength = 0
};

static uint32_t tableOfContents[VOICE_PROMPTS_TOC_SIZE];
static bool     vpDataLoaded      = false;
static bool     voicePromptActive = false;

static beepData_t beepSeriesBuffer[BEEP_SEQ_BUF_SIZE];
static uint16_t   currentBeepDuration = 0;
static uint8_t    beepSeriesIndex     = 0;
static bool       delayBeepUntilTick  = false;

static pathId     vpAudioPath;
static long long  vpStartTime;

#ifdef VP_USE_FILESYSTEM
static FILE *vpFile = NULL;
#else
extern unsigned char _vpdata_start;
extern unsigned char _vpdata_end;
unsigned char        *vpData = &_vpdata_start;
#endif

/**
 * \internal
 * Load voice prompts header.
 *
 * @param header: pointer toa vpHeader_t data structure.
 */
static void loadVpHeader(vpHeader_t *header)
{
    #ifdef VP_USE_FILESYSTEM
    fseek(vpFile, 0L, SEEK_SET);
    fread(header, sizeof(vpHeader_t), 1, vpFile);
    #else
    memcpy(header, vpData, sizeof(vpHeader_t));
    #endif
}

/**
 * \internal
 * Load voice prompts' table of contents.
 */
static void loadVpToC()
{
    #ifdef VP_USE_FILESYSTEM
    fread(&tableOfContents, sizeof(tableOfContents), 1, vpFile);
    size_t vpDataOffset = ftell(vpFile);

    if(vpDataOffset == (sizeof(vpHeader_t) + sizeof(tableOfContents)))
        vpDataLoaded = true;
    #else
    uint8_t *tocPtr = vpData + sizeof(vpHeader_t);
    memcpy(&tableOfContents, tocPtr, sizeof(tableOfContents));
    vpDataLoaded = true;
    #endif
}

/**
 * \internal
 * Load Codec2 data for a voice prompt.
 *
 * @param offset: offset relative to the start of the voice prompt data.
 * @param length: data length in bytes.
 */
static void fetchCodec2Data(uint8_t *data, const size_t offset)
{
    if (vpDataLoaded == false)
        return;

    #ifdef VP_USE_FILESYSTEM
    if (vpFile == NULL)
        return;

    size_t start = sizeof(vpHeader_t)
                 + sizeof(tableOfContents)
                 + CODEC2_HEADER_SIZE;

    fseek(vpFile, start + offset, SEEK_SET);
    fread(data, 8, 1, vpFile);
    #else
    uint8_t *dataPtr = vpData
                     + sizeof(vpHeader_t)
                     + sizeof(tableOfContents)
                     + CODEC2_HEADER_SIZE;

    if((dataPtr + 8) >= &_vpdata_end)
    {
        memset(data, 0x00, 8);
        return;
    }

    memcpy(data, dataPtr + offset, 8);
    #endif
}

/**
 * \internal
 * Check validity of voice prompt header.
 *
 * @param header: voice prompt header to be checked.
 * @return true if header is valid
 */
static inline bool checkVpHeader(const vpHeader_t* header)
{
    return ((header->magic == VOICE_PROMPTS_DATA_MAGIC) &&
            (header->version == VOICE_PROMPTS_DATA_VERSION));
}

/**
 * \internal
 * Perform a string lookup inside user dictionary.
 *
 * @param ptr: string to be searched.
 * @param advanceBy: final offset with respect of dictionary beginning.
 * @return index of user dictionary's voice prompt.
 */
static uint16_t userDictLookup(const char* ptr, int* advanceBy)
{
    if ((ptr == NULL) || (*ptr == '\0'))
        return 0;

    for(uint32_t index = 0; userDictionary[index].userWord != 0; index++)
    {
        int len = strlen(userDictionary[index].userWord);
        if (strncasecmp(userDictionary[index].userWord, ptr, len) == 0)
        {
            *advanceBy = len;
            return userDictionary[index].vp;
        }
    }

    return 0;
}

/**
 * \internal
 *
 */
static bool GetSymbolVPIfItShouldBeAnnounced(char symbol,
                                             vpFlags_t flags,
                                             voicePrompt_t* vp)
{
    *vp = PROMPT_SILENCE;

    const char indexedSymbols[] =
        "%.+-*#!,@:?()~/[]<>=$'`&|_^{}";  // Must match order of symbols in
                                          // voicePrompt_t enum.
    const char commonSymbols[] = "%.+-*#";

    bool announceCommonSymbols =
        (flags & vpAnnounceCommonSymbols) ? true : false;
    bool announceLessCommonSymbols =
        (flags & vpAnnounceLessCommonSymbols) ? true : false;

    char* symbolPtr = strchr(indexedSymbols, symbol);

    if (symbolPtr == NULL)
    {  // we don't have a prompt for this character.
        return (flags & vpAnnounceASCIIValueForUnknownChars) ? true : false;
    }

    bool commonSymbol = strchr(commonSymbols, symbol) != NULL;

    *vp = PROMPT_PERCENT + (symbolPtr - indexedSymbols);

    return ((commonSymbol && announceCommonSymbols) ||
            (!commonSymbol && announceLessCommonSymbols));
}

/**
 * \internal
 * Function managing set up of audio path towards the speaker.
 */
static inline void enableSpkOutput()
{
    // Set up a new audio path only if there is no other one already open to
    // avoid overwriting the path ID with a -1, locking everything.
    if(audioPath_getStatus(vpAudioPath) == PATH_CLOSED)
    {
        vpAudioPath = audioPath_request(SOURCE_MCU, SINK_SPK, PRIO_PROMPT);
    }
}

/**
 * \internal
 * Function managing release of audio path towards the speaker.
 */
static inline void disableSpkOutput()
{
    // Avoid chomping away a still in-progress beep or voice prompt.
    if((currentBeepDuration != 0) || (voicePromptActive == true))
        return;

    audioPath_release(vpAudioPath);
}

/**
 * \internal
 * Stop an ongoing beep, if present, and clear all the beep management
 * variables.
 */
static void beep_flush()
{
    if (currentBeepDuration > 0)
        platform_beepStop();

    memset(beepSeriesBuffer, 0, sizeof(beepSeriesBuffer));
    currentBeepDuration = 0;
    beepSeriesIndex     = 0;
    disableSpkOutput();
}

/**
 * \internal
 * Function managing beep update.
 */
static bool beep_tick()
{
    if (currentBeepDuration > 0)
    {
        if (delayBeepUntilTick)
        {
            platform_beepStart(beepSeriesBuffer[beepSeriesIndex].freq);
            delayBeepUntilTick = false;
        }

        currentBeepDuration--;
        if (currentBeepDuration == 0)
        {
            platform_beepStop();

            // see if there are any more in the series to play.
            if ((beepSeriesBuffer[beepSeriesIndex+1].freq     != 0) &&
                (beepSeriesBuffer[beepSeriesIndex+1].duration != 0))
            {
                beepSeriesIndex++;
                currentBeepDuration = beepSeriesBuffer[beepSeriesIndex].duration;
                platform_beepStart(beepSeriesBuffer[beepSeriesIndex].freq);
            }
            else
            {
                // Clear all variables for beep management
                beep_flush();
            }
        }

        return true;
    }

    return false;
}


void vp_init()
{
    #ifdef VP_USE_FILESYSTEM
    if(vpFile == NULL)
        vpFile = fopen("voiceprompts.vpc", "r");

    if(vpFile == NULL)
        return;
    #else
    if(&_vpdata_start == &_vpdata_end)
        return;
    #endif

    // Read header
    vpHeader_t header;
    loadVpHeader(&header);

    if(checkVpHeader(&header) == true)
    {
        loadVpToC();
    }

    if (vpDataLoaded)
    {
        // If the hash key is down, set vpLevel to high, if beep or less.
        if ((kbd_getKeys() & KEY_HASH) && (state.settings.vpLevel <= vpBeep))
            state.settings.vpLevel = vpHigh;
    }
    else
    {
        // ensure we at least have beeps in the event no voice prompts are
        // loaded.
        if (state.settings.vpLevel > vpBeep)
            state.settings.vpLevel = vpBeep;
    }

    // Initialize codec2 module
    codec_init();
}

void vp_terminate()
{
    if (voicePromptActive)
        vp_flush();

    codec_terminate();

    #ifdef VP_USE_FILESYSTEM
    fclose(vpFile);
    #endif
}

void vp_stop()
{
    voicePromptActive = false;
    codec_stop(vpAudioPath);
    disableSpkOutput();

    // Clear voice prompt sequence data
    vpCurrentSequence.pos          = 0;
    vpCurrentSequence.c2DataIndex  = 0;
    vpCurrentSequence.c2DataLength = 0;

    // If any beep is playing, immediately stop it.
    beep_flush();
}

void vp_flush()
{
    // Stop the prompt and reset the codec data length
    vp_stop();
    vpCurrentSequence.length = 0;
}

void vp_queuePrompt(const uint16_t prompt)
{
    if (state.settings.vpLevel < vpLow)
        return;

    if (voicePromptActive)
        vp_flush();

    if (vpCurrentSequence.length < VP_SEQUENCE_BUF_SIZE)
    {
        vpCurrentSequence.buffer[vpCurrentSequence.length] = prompt;
        vpCurrentSequence.length++;
    }
}

void vp_queueString(const char* string, vpFlags_t flags)
{
    if (state.settings.vpLevel < vpLow)
        return;

    if (voicePromptActive)
        vp_flush();

    if (state.settings.vpPhoneticSpell)
        flags |= vpAnnouncePhoneticRendering;

    while (*string != '\0')
    {
        int advanceBy    = 0;
        voicePrompt_t vp = userDictLookup(string, &advanceBy);

        if (vp != 0)
        {
            vp_queuePrompt(vp);
            string += advanceBy;
            continue;
        }
        else if ((*string >= '0') && (*string <= '9'))
        {
            vp_queuePrompt(*string - '0' + PROMPT_0);
        }
        else if ((*string >= 'A') && (*string <= 'Z'))
        {
            if (flags & vpAnnounceCaps)
                vp_queuePrompt(PROMPT_CAP);
            if (flags & vpAnnouncePhoneticRendering)
                vp_queuePrompt((*string - 'A') + PROMPT_A_PHONETIC);
            else
                vp_queuePrompt(*string - 'A' + PROMPT_A);
        }
        else if ((*string >= 'a') && (*string <= 'z'))
        {
            if (flags & vpAnnouncePhoneticRendering)
                vp_queuePrompt((*string - 'a') + PROMPT_A_PHONETIC);
            else
                vp_queuePrompt(*string - 'a' + PROMPT_A);
        }
        else if ((*string == ' ') && (flags & vpAnnounceSpace))
        {
            vp_queuePrompt(PROMPT_SPACE);
        }
        else if (GetSymbolVPIfItShouldBeAnnounced(*string, flags, &vp))
        {
            if (vp != PROMPT_SILENCE)
                vp_queuePrompt(vp);
            else
            {
                // announce ASCII
                int32_t val = *string;
                vp_queuePrompt(PROMPT_CHARACTER);
                vp_queueInteger(val);
            }
        }
        else
        {
            // otherwise just add silence
            vp_queuePrompt(PROMPT_SILENCE);
        }

        string++;
    }

    if (flags & vpqAddSeparatingSilence)
        vp_queuePrompt(PROMPT_SILENCE);
}

void vp_queueInteger(const int value)
{
    if (state.settings.vpLevel < vpLow)
        return;

    char buf[12] = {0};  // min: -2147483648, max: 2147483647
    if (value < 0)
        vp_queuePrompt(PROMPT_MINUS);

    sniprintf(buf, 12, "%d", value);
    vp_queueString(buf, 0);
}

void vp_queueStringTableEntry(const char* const* stringTableStringPtr)
{
    /*
     * This function looks up a voice prompt corresponding to a string table
     * entry. These are stored in the voice data after the voice prompts with no
     * corresponding string table entry, hence the offset calculation:
     * NUM_VOICE_PROMPTS + (stringTableStringPtr - currentLanguage->languageName)
     */

    if (state.settings.vpLevel < vpLow)
        return;

    if (stringTableStringPtr == NULL)
        return;

    uint16_t pos = NUM_VOICE_PROMPTS
                 + (stringTableStringPtr - &currentLanguage->languageName);

    vp_queuePrompt(pos);
}

void vp_play()
{
    if (state.settings.vpLevel < vpLow)
        return;

    if (voicePromptActive)
        return;

    if (vpCurrentSequence.length <= 0)
        return;

    // TODO: remove this once switching to hardware-based I2C driver for AT1846S
    // management.
    vpStartTime = getTick();
}

void vp_tick()
{
    if (platform_getPttStatus() && (voicePromptActive || (currentBeepDuration > 0)))
    {
        vp_stop();
        return;
    }

    if (beep_tick())
        return;

    // Temporary fix for the following bug: on MD-UV3x0 the configuration of
    // the AT1846S chip may take more than 20ms, making the codec2 thread miss
    // the syncronization point with the output stream. By delaying the start
    // of the voice prompt by 50ms, a time span almost not noticeable, we avoid
    // to incur in such a problem.
    // TODO: remove this once switched to hardware-based I2C driver for AT1846S
    // management.
    if((vpStartTime > 0) && ((getTick() - vpStartTime) > 50))
    {
        vpStartTime       = 0;
        voicePromptActive = true;
        enableSpkOutput();
        codec_startDecode(vpAudioPath);
    }

    if (voicePromptActive == false)
        return;

    while(vpCurrentSequence.pos < vpCurrentSequence.length)
    {
        // get the codec2 data for the current prompt if needed.
        if (vpCurrentSequence.c2DataLength == 0)
        {
            // obtain the data for the prompt.
            int promptNumber = vpCurrentSequence.buffer[vpCurrentSequence.pos];

            vpCurrentSequence.c2DataIndex  = 0;
            vpCurrentSequence.c2DataStart  = tableOfContents[promptNumber];
            vpCurrentSequence.c2DataLength = ((tableOfContents[promptNumber + 1]
                                           - tableOfContents[promptNumber])/8 * 8);
        }

        while (vpCurrentSequence.c2DataIndex < vpCurrentSequence.c2DataLength)
        {
            // push the codec2 data in lots of 8 byte frames.
            uint8_t c2Frame[8] = {0};

            fetchCodec2Data(c2Frame, vpCurrentSequence.c2DataStart +
                                     vpCurrentSequence.c2DataIndex);

            // Do not push codec2 data if audio path is closed or suspended
            if(audioPath_getStatus(vpAudioPath) != PATH_OPEN)
                return;

            if(codec_pushFrame(c2Frame, false) < 0)
                return;

            vpCurrentSequence.c2DataIndex += 8;
        }

        vpCurrentSequence.pos++;            // ready for next prompt in sequence.
        vpCurrentSequence.c2DataLength = 0; // flag that we need to get more data.
        vpCurrentSequence.c2DataIndex  = 0;
    }

    // see if we've finished.
    if(vpCurrentSequence.pos == vpCurrentSequence.length)
    {
        voicePromptActive              = false;
        vpCurrentSequence.pos          = 0;
        vpCurrentSequence.c2DataIndex  = 0;
        vpCurrentSequence.c2DataLength = 0;
        codec_stop(vpAudioPath);
        disableSpkOutput();
    }
}

bool vp_isPlaying()
{
    return voicePromptActive;
}

bool vp_sequenceNotEmpty()
{
    return (vpCurrentSequence.length > 0);
}

void vp_beep(uint16_t freq, uint16_t duration)
{
    if (state.settings.vpLevel < vpBeep)
        return;

    // Do not play a new one if one is playing.
    if (currentBeepDuration != 0)
        return;

    // avoid extra long beeps!
    if (duration > 20)
        duration = 20;

    beepSeriesBuffer[0].freq     = freq;
    beepSeriesBuffer[0].duration = duration;
    beepSeriesBuffer[1].freq     = 0;
    beepSeriesBuffer[1].duration = 0;
    currentBeepDuration = duration;
    beepSeriesIndex     = 0;
    platform_beepStart(freq);
    enableSpkOutput();
}

void vp_beepSeries(const uint16_t* beepSeries)
{
    if (state.settings.vpLevel < vpBeep)
        return;

    if (currentBeepDuration != 0)
        return;

    enableSpkOutput();

    if (beepSeries == NULL)
        return;

    memcpy(beepSeriesBuffer, beepSeries, BEEP_SEQ_BUF_SIZE*sizeof(beepData_t));
    // Always ensure that the array is terminated!
    beepSeriesBuffer[BEEP_SEQ_BUF_SIZE-1].freq     = 0;
    beepSeriesBuffer[BEEP_SEQ_BUF_SIZE-1].duration = 0;

    currentBeepDuration = beepSeriesBuffer[0].duration;
    beepSeriesIndex     = 0;
    delayBeepUntilTick  = true;
}
