/***************************************************************************
 *   Copyright (C) 2022 - 2023 by Federico Amedeo Izzo IU2NUO,             *
 *                                Niccolò Izzo IU2KIN,                     *
 *                                Silvano Seva IU2KWO                      *
 *                                Joseph Stephen VK7JS                     *
 *                                                                         *
 *   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/>   *
 ***************************************************************************/

/**
 * This file contains functions for announcing radio functions using the
 * building blocks in voicePrompts.h/c.
 */

#include "core/voicePromptUtils.h"

#include <state.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <utils.h>
#include <inttypes.h>
#include <ui/ui_default.h>
#include <beeps.h>
#include "interfaces/cps_io.h"

const uint16_t BOOT_MELODY[] = {400, 3, 600, 3, 800, 3, 0, 0};

static void clearCurrPromptIfNeeded(const vpQueueFlags_t flags)
{
    if (flags & vpqInit)
        vp_flush();
}

static void playIfNeeded(const vpQueueFlags_t flags)
{
    uint8_t vpLevel = state.settings.vpLevel;

    if ((flags & vpqPlayImmediately) ||
        ((flags & vpqPlayImmediatelyAtMediumOrHigher) && (vpLevel >= vpMedium)))
        vp_play();
}

static void addSilenceIfNeeded(const vpQueueFlags_t flags)
{
    if ((flags & vpqAddSeparatingSilence) == 0)
        return;

    vp_queuePrompt(PROMPT_SILENCE);
    vp_queuePrompt(PROMPT_SILENCE);
}



void vp_announceChannelName(const channel_t* channel,
                            const uint16_t channelNumber,
                            const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_CHANNEL);
    }

    vp_queueInteger(channelNumber);

    // Only queue the name if it is not the same as the raw number.
    // Otherwise the radio will repeat  channel 1 channel 1 for channel 1.
    char numAsStr[16] = "\0";
    sniprintf(numAsStr, 16, "Channel%d", channelNumber);

    if (strcmp(numAsStr, channel->name) != 0)
    {
        vp_queueString(channel->name, vpAnnounceCommonSymbols);
    }

    playIfNeeded(flags);
}

void vp_queueFrequency(const freq_t freq)
{
    char buffer[16];
    int MHz = (freq / 1000000);
    int kHz = ((freq % 1000000) / 100);

    sniprintf(buffer, 16, "%d.%05d", MHz, kHz);

    stripTrailingZeroes(buffer);

    vp_queueString(buffer, vpAnnounceCommonSymbols);
    vp_queuePrompt(PROMPT_MEGAHERTZ);
}

void vp_announceFrequencies(const freq_t rx, const freq_t tx,
                            const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    // If rx and tx frequencies differ, announce both, otherwise just one
    if (rx == tx)
    {
        vp_queueFrequency(rx);
    }
    else
    {
        vp_queuePrompt(PROMPT_RECEIVE);
        vp_queueFrequency(rx);
        vp_queuePrompt(PROMPT_TRANSMIT);
        vp_queueFrequency(tx);
    }

    playIfNeeded(flags);
}

void vp_announceRadioMode(const uint8_t mode, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_MODE);
    }

    switch (mode)
    {
        case OPMODE_DMR:
            vp_queueStringTableEntry(&currentLanguage->dmr);
            break;

        case OPMODE_FM:
            vp_queueStringTableEntry(&currentLanguage->fm);
            break;

        case OPMODE_M17:
            vp_queueStringTableEntry(&currentLanguage->m17);
            break;
    }

    playIfNeeded(flags);
}

void vp_announceBandwidth(const uint8_t bandwidth, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_BANDWIDTH);
    }

    char* bandwidths[] = {"12.5", "25"};
    vp_queueString(bandwidths[bandwidth], vpAnnounceCommonSymbols);
    vp_queuePrompt(PROMPT_KILOHERTZ);
    playIfNeeded(flags);
}

void vp_announcePower(const uint32_t power, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_POWER);
    }

    // Compute x.y format avoiding to pull in floating point math.
    // Remember that power is expressed in mW!
    char buffer[16] = "\0";
    sniprintf(buffer, 16, "%lu.%lu", (power / 1000lu), (power % 1000lu) / 100lu);

    vp_queueString(buffer, vpAnnounceCommonSymbols);
    vp_queuePrompt(PROMPT_WATTS);
    playIfNeeded(flags);
}

void vp_announceChannelSummary(const channel_t* channel,
                               const uint16_t channelNumber, const uint16_t bank,
                               const vpSummaryInfoFlags_t infoFlags)
{
    if (channel == NULL)
        return;

    vp_flush();

    vpQueueFlags_t localFlags = vpqAddSeparatingSilence;

    // Force on the descriptions for level 3.
    if (state.settings.vpLevel == vpHigh)
    {
        localFlags |= vpqIncludeDescriptions;
    }

    // If VFO mode, announce VFO.
    // channelNumber will be 0 if called from VFO mode.
    if ((infoFlags & vpChannelNameOrVFO) != 0)
    {
        if (channelNumber == 0)
        {
            vp_queuePrompt(PROMPT_VFO);
        }
        else
        {
            vp_announceChannelName(channel, channelNumber, localFlags);
        }
        addSilenceIfNeeded(localFlags);
    }

    if ((infoFlags & vpFrequencies) != 0)
        vp_announceFrequencies(channel->rx_frequency, channel->tx_frequency,
                               localFlags);

    if ((infoFlags & vpRadioMode) != 0)
    {
        vp_announceRadioMode(channel->mode, localFlags);
        addSilenceIfNeeded(localFlags);
    }

    if ((infoFlags & vpModeSpecificInfo) != 0)
    {
        switch(channel->mode)
        {
            case OPMODE_FM:
            {
                vp_announceBandwidth(channel->bandwidth, localFlags);
                addSilenceIfNeeded(localFlags);

                if (channel->fm.rxToneEn || channel->fm.txToneEn)
                {
                    vp_announceCTCSS(channel->fm.rxToneEn, channel->fm.rxTone,
                                     channel->fm.txToneEn, channel->fm.txTone,
                                     localFlags);
                }
            }
                break;

            case OPMODE_M17:
                vp_announceM17Info(channel, false, localFlags);
                break;

            case OPMODE_DMR:
            {
                vp_announceContactWithIndex(channel->dmr.contact_index,
                                            localFlags);

                // Force announcement of the words timeslot and colorcode to avoid
                // ambiguity.
                vp_announceTimeslot(channel->dmr.dmr_timeslot,
                                   (localFlags | vpqIncludeDescriptions));
                vp_announceColorCode(channel->dmr.rxColorCode,
                                     channel->dmr.txColorCode,
                                    (localFlags | vpqIncludeDescriptions));
            }
                break;
        }

        addSilenceIfNeeded(localFlags);
    }

    if ((infoFlags & vpPower) != 0)
    {
        vp_announcePower(channel->power, localFlags);
        addSilenceIfNeeded(localFlags);
    }

    if (((infoFlags & vpBankNameOrAllChannels) != 0) && (channelNumber > 0))  // i.e. not called from VFO.
    {
        vp_announceBank(bank, localFlags);
    }

    vp_play();
}

void vp_announceInputChar(const char ch)
{
    char buf[2] = "\0";
    buf[0]      = ch;

    vp_flush();

    uint8_t flags = vpAnnounceSpace
                  | vpAnnounceCommonSymbols
                  | vpAnnounceLessCommonSymbols;

    vp_queueString(buf, flags);
    vp_play();
}

void vp_announceInputReceiveOrTransmit(const bool tx, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (tx)
        vp_queuePrompt(PROMPT_TRANSMIT);
    else
        vp_queuePrompt(PROMPT_RECEIVE);

    playIfNeeded(flags);
}

void vp_replayLastPrompt()
{
    if (vp_isPlaying())
        vp_stop();
    else
        vp_play();
}

void vp_announceError(const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);
    vp_queueStringTableEntry(&currentLanguage->error);
    playIfNeeded(flags);
}

void vp_announceText(const char* text, const vpQueueFlags_t flags)
{
    if ((text == NULL) || (*text == '\0'))
        return;

    clearCurrPromptIfNeeded(flags);

    // See if we have a prompt for this string.
    int offset = GetEnglishStringTableOffset(text);

    if (offset != -1)
        vp_queueStringTableEntry(
            (const char* const*)(&currentLanguage->languageName + offset));
    else  // Just spell it out
        vp_queueString(text, vpAnnounceCommonSymbols);

    playIfNeeded(flags);
}

void vp_announceCTCSS(const bool rxToneEnabled, const uint8_t rxTone,
                      const bool txToneEnabled, const uint8_t txTone,
                      const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if ((rxToneEnabled == false) && (txToneEnabled == false))
    {
        if (flags & vpqIncludeDescriptions)
            vp_queuePrompt(PROMPT_TONE);

        vp_queueStringTableEntry(&currentLanguage->off);
        playIfNeeded(flags);
        return;
    }

    char buffer[16] = "\0";

    // If the rx and tx tones are the same and both are enabled, just say Tone.
    if ((rxToneEnabled && txToneEnabled) && (rxTone == txTone))
    {
        if (flags & vpqIncludeDescriptions)
            vp_queuePrompt(PROMPT_TONE);

        uint16_t tone = ctcss_tone[rxTone];
        sniprintf(buffer, sizeof(buffer), "%d.%d", (tone / 10), (tone % 10));

        vp_queueString(buffer, vpAnnounceCommonSymbols);
        vp_queuePrompt(PROMPT_HERTZ);
        playIfNeeded(flags);

        return;
    }

    // Speak the individual rx and tx tones.
    if (rxToneEnabled)
    {
        if (flags & vpqIncludeDescriptions)
        {
            vp_queuePrompt(PROMPT_RECEIVE);
            vp_queuePrompt(PROMPT_TONE);
        }

        uint16_t tone = ctcss_tone[rxTone];
        sniprintf(buffer, sizeof(buffer), "%d.%d", (tone / 10), (tone % 10));

        vp_queueString(buffer, vpAnnounceCommonSymbols);
        vp_queuePrompt(PROMPT_HERTZ);
    }
    if (txToneEnabled)
    {
        if (flags & vpqIncludeDescriptions)
        {
            vp_queuePrompt(PROMPT_TRANSMIT);
            vp_queuePrompt(PROMPT_TONE);
        }

        uint16_t tone = ctcss_tone[txTone];
        sniprintf(buffer, sizeof(buffer), "%d.%d", (tone / 10), (tone % 10));

        vp_queueString(buffer, vpAnnounceCommonSymbols);
        vp_queuePrompt(PROMPT_HERTZ);
    }

    playIfNeeded(flags);
}

void vp_announceSquelch(const uint8_t squelch, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_SQUELCH);
    }

    vp_queueInteger(squelch);
    playIfNeeded(flags);
}

void vp_announceContact(const contact_t* contact, const vpQueueFlags_t flags)
{
    if (contact == NULL)
        return;

    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_CONTACT);
    }

    if (contact->name[0] != '\0')
    {
        vp_queueString(contact->name, vpAnnounceCommonSymbols);
    }

    playIfNeeded(flags);
}

bool vp_announceContactWithIndex(const uint16_t index, const vpQueueFlags_t flags)
{
    if (index == 0)
        return false;

    contact_t contact;
    if (cps_readContact(&contact, index) == -1)
        return false;

    vp_announceContact(&contact, flags);

    return true;
}

void vp_announceTimeslot(const uint8_t timeslot, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_TIMESLOT);
    }

    vp_queueInteger(timeslot);
    playIfNeeded(flags);
}

void vp_announceColorCode(const uint8_t rxColorCode, const uint8_t txColorCode,
                          const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_COLORCODE);
    }

    if (rxColorCode == txColorCode)
    {
        vp_queueInteger(rxColorCode);
    }
    else
    {
        vp_queuePrompt(PROMPT_RECEIVE);
        vp_queueInteger(rxColorCode);
        vp_queuePrompt(PROMPT_TRANSMIT);
        vp_queueInteger(txColorCode);
    }

    playIfNeeded(flags);
}

void vp_announceBank(const uint16_t bank, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (state.bank_enabled)
    {
        bankHdr_t bank_hdr = {0};
        cps_readBankHeader(&bank_hdr, bank);
        vp_queueString(bank_hdr.name, vpAnnounceCommonSymbols);
    }
    else
    {
        vp_queueStringTableEntry(&currentLanguage->allChannels);
    }

    playIfNeeded(flags);
}

void vp_announceM17Info(const channel_t* channel, bool isEditing,
                        const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queuePrompt(PROMPT_DEST_ID);
    }

    if (isEditing)
    {
        vp_queuePrompt(PROMPT_EDIT);
    }
    else if (state.settings.m17_dest[0] != '\0')
    {
        vp_queueString(state.settings.m17_dest, vpAnnounceCommonSymbols);
    }
    else if ((channel != NULL) && (channel->m17.contact_index != 0))
    {
        if (!vp_announceContactWithIndex(channel->m17.contact_index, flags))
                    vp_queueStringTableEntry(&currentLanguage->broadcast);
    }
    else
    {
        vp_queueStringTableEntry(&currentLanguage->broadcast);
    }

    playIfNeeded(flags);
}

#ifdef CONFIG_GPS
// cardinal point plus or minus this value is still considered cardinal point.
#define margin 3

static bool IsCompassCloseEnoughToCardinalPoint()
{
    float tmg_true = state.gps_data.tmg_true;

    return (tmg_true < (0   + margin) || tmg_true > (360 - margin)) || // north
           (tmg_true > (90  - margin) && tmg_true < (90  + margin)) || // east
           (tmg_true > (180 - margin) && tmg_true < (180 + margin)) || // south
           (tmg_true > (270 - margin) && tmg_true < (270 + margin)) || // west
           (tmg_true > (45  - margin) && tmg_true < (45  + margin)) || // n.w.
           (tmg_true > (135 - margin) && tmg_true < (135 + margin)) || // s.e.
           (tmg_true > (225 - margin) && tmg_true < (225 + margin)) || // s.w.
           (tmg_true > (315 - margin) && tmg_true < (315 + margin));   // n.w.
}

void vp_announceGPSInfo(vpGPSInfoFlags_t gpsInfoFlags)
{
    vp_flush();
    vpQueueFlags_t flags = vpqIncludeDescriptions
                         | vpqAddSeparatingSilence;

    if (gpsInfoFlags & vpGPSIntro)
    {
        vp_queueStringTableEntry(&currentLanguage->gps);
        if (!state.settings.gps_enabled)
        {
            vp_queueStringTableEntry(&currentLanguage->off);
            vp_play();

            return;
        }
    }

    if (gpsInfoFlags & vpGPSFixQuality)
    {
        switch (state.gps_data.fix_quality)
        {
            case 0:
                vp_queueStringTableEntry(&currentLanguage->noFix);
                vp_play();
                return;
            case 1:
                vp_queueString("SPS", vpAnnounceCommonSymbols);
                break;

            case 2:
                vp_queueString("DGPS", vpAnnounceCommonSymbols);
                break;

            case 3:
                vp_queueString("PPS", vpAnnounceCommonSymbols);
                break;

            case 6:
                vp_queueStringTableEntry(&currentLanguage->fixLost);
                break;

            default:
                vp_queueStringTableEntry(&currentLanguage->error);

                vp_play();

                return;
        }

        addSilenceIfNeeded(flags);
    }

    if (gpsInfoFlags & vpGPSFixType)
    {
        switch (state.gps_data.fix_type)
        {
            case 2:
                vp_queueString("2D", vpAnnounceCommonSymbols);
                break;

            case 3:
                vp_queueString("3D", vpAnnounceCommonSymbols);
                break;
        }

        addSilenceIfNeeded(flags);
    }

    char buffer[17] = "\0";

    if (gpsInfoFlags & vpGPSDirection)
    {
        vp_queuePrompt(PROMPT_COMPASS);
        if (!IsCompassCloseEnoughToCardinalPoint())
        {
            sniprintf(buffer, 16, "%3.1f", state.gps_data.tmg_true);
            vp_queueString(buffer, vpAnnounceCommonSymbols);
            vp_queuePrompt(PROMPT_DEGREES);
        }

        if ((state.gps_data.tmg_true < (45  + margin)) ||
            (state.gps_data.tmg_true > (315 - margin)))
        {
            vp_queuePrompt(PROMPT_NORTH);
        }

        if ((state.gps_data.tmg_true > (45 - margin)) &&
            (state.gps_data.tmg_true < (135 + margin)))
        {
            vp_queuePrompt(PROMPT_EAST);
        }

        if ((state.gps_data.tmg_true > (135 - margin)) &&
            (state.gps_data.tmg_true < (225 + margin)))
        {
            vp_queuePrompt(PROMPT_SOUTH);
        }

        if ((state.gps_data.tmg_true > (225 - margin)) &&
            (state.gps_data.tmg_true < (315 + margin)))
        {
            vp_queuePrompt(PROMPT_WEST);
        }

        addSilenceIfNeeded(flags);
    }

    if ((gpsInfoFlags & vpGPSSpeed) != 0)
    {
        // speed/altitude:
        sniprintf(buffer, 16, "%dkm/h", state.gps_data.speed);
        vp_queuePrompt(PROMPT_SPEED);
        vp_queueString(buffer, vpAnnounceCommonSymbols |
                               vpAnnounceLessCommonSymbols);
    }

    if ((gpsInfoFlags & vpGPSAltitude) != 0)
    {
        vp_queuePrompt(PROMPT_ALTITUDE);

        sniprintf(buffer, 16, "%dm", state.gps_data.altitude);
        vp_queueString(buffer, vpAnnounceCommonSymbols);
        addSilenceIfNeeded(flags);
    }

    if ((gpsInfoFlags & vpGPSLatitude) != 0)
    {
        // Convert from signed longitude, to unsigned + direction
        int32_t latitude        = abs(state.gps_data.latitude);
        uint8_t latitude_int    = latitude / 1000000;
        int32_t latitude_dec    = latitude % 1000000;
        voicePrompt_t direction = (state.gps_data.latitude < 0) ? PROMPT_SOUTH : PROMPT_NORTH;
        sniprintf(buffer, 16, "%d.%06"PRId32, latitude_int, latitude_dec);
        stripTrailingZeroes(buffer);
        vp_queuePrompt(PROMPT_LATITUDE);
        vp_queueString(buffer, vpAnnounceCommonSymbols);
        vp_queuePrompt(direction);
    }

    if ((gpsInfoFlags & vpGPSLongitude) != 0)
    {
        // Convert from signed longitude, to unsigned + direction
        int32_t longitude       = abs(state.gps_data.longitude);
        uint8_t longitude_int   = longitude / 1000000;
        int32_t longitude_dec   = longitude % 1000000;
        voicePrompt_t direction = (state.gps_data.longitude < 0) ? PROMPT_WEST : PROMPT_EAST;
        sniprintf(buffer, 16, "%d.%06"PRId32, longitude_int, longitude_dec);
        stripTrailingZeroes(buffer);

        vp_queuePrompt(PROMPT_LONGITUDE);
        vp_queueString(buffer, vpAnnounceCommonSymbols);
        vp_queuePrompt(direction);
        addSilenceIfNeeded(flags);
    }

    if ((gpsInfoFlags & vpGPSSatCount) != 0)
    {
        vp_queuePrompt(PROMPT_SATELLITES);
        vp_queueInteger(state.gps_data.satellites_in_view);
    }

    vp_play();
}
#endif // CONFIG_GPS

void vp_announceAboutScreen()
{
    vp_flush();

    vp_queueStringTableEntry(&currentLanguage->openRTX);

    vp_queueStringTableEntry(&currentLanguage->Niccolo);
    vp_queueStringTableEntry(&currentLanguage->Silvano);
    vp_queueStringTableEntry(&currentLanguage->Federico);
    vp_queueStringTableEntry(&currentLanguage->Fred);
    vp_queueStringTableEntry(&currentLanguage->Joseph);

    vp_play();
}

void vp_announceBackupScreen()
{
    vp_flush();

    vp_queueStringTableEntry(&currentLanguage->flashBackup);

    vp_queueStringTableEntry(&currentLanguage->connectToRTXTool);
    vp_queueStringTableEntry(&currentLanguage->toBackupFlashAnd);
    vp_queueStringTableEntry(&currentLanguage->pressPTTToStart);
    vp_queuePrompt(PROMPT_VP_UNAVAILABLE);

    vp_play();
}

void vp_announceRestoreScreen()
{
    vp_flush();

    vp_queueStringTableEntry(&currentLanguage->flashRestore);

    vp_queueStringTableEntry(&currentLanguage->connectToRTXTool);
    vp_queueStringTableEntry(&currentLanguage->toRestoreFlashAnd);
    vp_queueStringTableEntry(&currentLanguage->pressPTTToStart);
    vp_queuePrompt(PROMPT_VP_UNAVAILABLE);

    vp_play();
}

#ifdef CONFIG_RTC
void vp_announceSettingsTimeDate()
{
    vp_flush();

    vp_queueStringTableEntry(&currentLanguage->timeAndDate);

    datetime_t local_time = utcToLocalTime(state.time,
                                           state.settings.utc_timezone);

    char buffer[16] = "\0";
    sniprintf(buffer, 16, "%02d/%02d/%02d", local_time.date, local_time.month,
                                           local_time.year);
    vp_queueString(buffer, vpAnnounceCommonSymbols |
                           vpAnnounceLessCommonSymbols);

    vp_queuePrompt(PROMPT_SILENCE);
    vp_queuePrompt(PROMPT_SILENCE);

    sniprintf(buffer, 16, "%02d:%02d:%02d", local_time.hour, local_time.minute,
                                           local_time.second);
    vp_queueString(buffer, vpAnnounceCommonSymbols |
                           vpAnnounceLessCommonSymbols);

    vp_play();
}
#endif // CONFIG_RTC

void vp_announceSettingsVoiceLevel(const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);
    switch (state.settings.vpLevel)
    {
        case vpNone:
            vp_queueStringTableEntry(&currentLanguage->off);
            break;

        case vpBeep:
            vp_queueStringTableEntry(&currentLanguage->beep);
            break;

        default:
            if (flags & vpqIncludeDescriptions)
            {
                vp_queuePrompt(PROMPT_VOICE_NAME);
                vp_queueStringTableEntry(&currentLanguage->level);
            }

            vp_queueInteger(state.settings.vpLevel-vpBeep);
            break;
    }

    playIfNeeded(flags);
}

void vp_announceSettingsOnOffToggle(const char* const* stringTableStringPtr,
                                    const vpQueueFlags_t flags, bool val)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
        vp_queueStringTableEntry(stringTableStringPtr);

    vp_queueStringTableEntry(val ? &currentLanguage->on : &currentLanguage->off);

     playIfNeeded(flags);
}

void vp_announceSettingsInt(const char* const* stringTableStringPtr,
                                    const vpQueueFlags_t flags,
                                    int val)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
        vp_queueStringTableEntry(stringTableStringPtr);

    vp_queueInteger(val);

    playIfNeeded(flags);
}

void vp_announceScreen(uint8_t ui_screen)
{
    const vpSummaryInfoFlags_t infoFlags = vpChannelNameOrVFO
                                         | vpFrequencies
                                         | vpRadioMode;

    switch (ui_screen)
    {
        case MAIN_VFO:
            vp_announceChannelSummary(&state.channel, 0, state.bank, infoFlags);
            break;

        case MAIN_MEM:
            vp_announceChannelSummary(&state.channel, state.channel_index+1,
                                      state.bank, infoFlags);
            break;

        #ifdef CONFIG_GPS
        case MENU_GPS:
            vp_announceGPSInfo(vpGPSAll);
            break;
        #endif

        case MENU_BACKUP:
            vp_announceBackupScreen();
            break;

        case MENU_RESTORE:
            vp_announceRestoreScreen();
            break;

        case MENU_ABOUT:
            vp_announceAboutScreen();
            break;

        #ifdef CONFIG_RTC
        case SETTINGS_TIMEDATE:
            vp_announceSettingsTimeDate();
            break;
        #endif

        case SETTINGS_M17:
            vp_announceBuffer(&currentLanguage->callsign,
                              false, true, state.settings.callsign);
            break;
    }
}

void vp_announceBuffer(const char* const* stringTableStringPtr,
                       bool editMode, bool callsign,
                       const char* buffer)
{
    bool isPlaying = vp_isPlaying();

    vp_flush();

    if (!isPlaying)
    {
        vp_queueStringTableEntry(stringTableStringPtr);

        if (editMode)
            vp_queuePrompt(PROMPT_EDIT);
    }

    vpFlags_t flags = vpAnnounceCommonSymbols;
    // add edit mode flags to adjust what is spoken.
    // extra symbols not relevant when entering callsign.
    if ((editMode == true) && (callsign == false))
        flags |= vpAnnounceLessCommonSymbols
              |  vpAnnounceSpace
              |  vpAnnounceASCIIValueForUnknownChars;

    vp_queueString(buffer, flags);

    vp_play();
}

void vp_announceDisplayTimer()
{
    bool isPlaying = vp_isPlaying();

    vp_flush();

    if (isPlaying == false)
        vp_queueStringTableEntry(&currentLanguage->timer);

    uint8_t seconds = 0;
    uint8_t minutes = 0;

    switch (state.settings.display_timer)
    {
        case TIMER_OFF:
            seconds = 0;
            break;

        case TIMER_5S:
        case TIMER_10S:
        case TIMER_15S:
        case TIMER_20S:
        case TIMER_25S:
        case TIMER_30S:
            seconds = state.settings.display_timer * 5;
            break;

        case TIMER_1M:
        case TIMER_2M:
        case TIMER_3M:
        case TIMER_4M:
        case TIMER_5M:
            minutes = (state.settings.display_timer - (TIMER_1M - 1));
            break;

        case TIMER_15M:
        case TIMER_30M:
        case TIMER_45M:
            minutes = 15 * (state.settings.display_timer - (TIMER_15M - 1));
            break;

        case TIMER_1H:
            minutes = 60;
            break;
    }

    if ((seconds == 0) && (minutes == 0))
    {
        vp_queueStringTableEntry(&currentLanguage->off);
    }
    else if (seconds > 0)
    {
        vp_queueInteger(seconds);
        vp_queuePrompt(PROMPT_SECONDS);
    }
    else if (minutes > 0)
    {
        vp_queueInteger(minutes);
        vp_queuePrompt(PROMPT_MINUTES);
    }

    vp_play();
}

vpQueueFlags_t vp_getVoiceLevelQueueFlags()
{
    uint8_t vpLevel = state.settings.vpLevel;
    vpQueueFlags_t flags = vpqInit
                         | vpqAddSeparatingSilence;

    switch (vpLevel)
    {
        case vpNone:
        case vpBeep:
            return vpqDefault;

        case vpLow:
            // Play some immediately, other things on demand.
            flags |= vpqPlayImmediatelyAtMediumOrHigher;
            break;

        case vpMedium:
            // Play all immediately but without extra descriptions
            flags |= vpqPlayImmediately;
            break;

        case vpHigh:
            // Play immediately with descriptions unless speech is in progress.
            flags |= vpqPlayImmediately;
            if (!vp_isPlaying())
                flags |= vpqIncludeDescriptions;
            break;
    }

    return flags;
}

void vp_playMenuBeepIfNeeded(bool firstItem)
{
// Since menus talk at levels above beep, there's no need to run this or you'll
// get an unwanted click.
    if (state.settings.vpLevel != vpBeep)
        return;
    if (firstItem)
        vp_beep(BEEP_MENU_FIRST_ITEM, SHORT_BEEP);
    else
        vp_beep(BEEP_MENU_ITEM, SHORT_BEEP);
}

void vp_announceSplashScreen()
{
    if (state.settings.vpLevel < vpBeep)
        return;

    vp_flush();

    if (state.settings.vpLevel == vpBeep)
    {
        vp_beepSeries(BOOT_MELODY);
        return;
    }

    vpQueueFlags_t localFlags = vpqAddSeparatingSilence;

    // Force on the descriptions for level 3.
    if (state.settings.vpLevel == vpHigh)
    {
        localFlags |= vpqIncludeDescriptions;
    }

    vp_queueStringTableEntry(&currentLanguage->openRTX);
    vp_queuePrompt(PROMPT_VFO);
    vp_announceFrequencies(state.channel.rx_frequency,
                           state.channel.tx_frequency,
                           localFlags);
    vp_announceRadioMode(state.channel.mode, localFlags);
    vp_play();
}

void vp_announceTimeZone(const int8_t timeZone, const vpQueueFlags_t flags)
{
    clearCurrPromptIfNeeded(flags);

    if (flags & vpqIncludeDescriptions)
    {
        vp_queueStringTableEntry(&currentLanguage->UTCTimeZone);
    }

    int wholeHours = timeZone / 2;
    int halfHour   = timeZone % 2;

    // While vp_queueInteeger handles negative numbers, we want to explicitly
    // say the sign even when positive.
    if (timeZone > 0)
    {
        vp_queuePrompt(PROMPT_PLUS);
    }
    else if (timeZone < 0)
    {
        vp_queuePrompt(PROMPT_MINUS);
        wholeHours *= -1;
    }

    vp_queueInteger(wholeHours);

    if (halfHour != 0)
    {
        vp_queuePrompt(PROMPT_POINT);
        vp_queueInteger(5);
    }

    playIfNeeded(flags);
}
