/*
 * Copyright (C) 2019-2025 Roger Clark, VK3KYY / G4KYF
 *                         Daniel Caujolle-Bert, F1RMB
 *
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
 *    in the documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
 *    incorporation in a commercial product or incorporation into a product or project which allows commercial use.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
#include "dmr_codec/codec.h"
#include "functions/sound.h"
#include "functions/voicePrompts.h"
#include "functions/settings.h"
#include "user_interface/uiLocalisation.h"
#include "functions/rxPowerSaving.h"
#include "hardware/radioHardwareInterface.h"

const uint32_t VOICE_PROMPTS_DATA_MAGIC = 0x5056;//'VP'
const uint32_t VOICE_PROMPTS_DATA_VERSION =
#if defined(HAS_COLOURS)
											0x0009; // Version 9 TOC increased to 368 (10 slots free)
													// Version 7 TOC increased to 350.
#define VOICE_PROMPTS_TOC_SIZE 				368
#else
											0x0008; // Version 8 TOC increased to 331 (10 slots free)
													// Version 6 TOC increased to 320. Added PROMOT_VOX and PROMPT_UNUSED_1 to PROMPT_UNUSED_10
#define VOICE_PROMPTS_TOC_SIZE 				331
#endif
													// Version 5 TOC increased to 300
													// Version 4 does not have unused items
                                                    // Version 3 does not have the PROMPT_TBD items in it

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

const uint32_t VOICE_PROMPTS_FLASH_HEADER_ADDRESS     = 0x8F400 + FLASH_ADDRESS_OFFSET;
const uint32_t VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS = 0xE0000 + FLASH_ADDRESS_OFFSET;
static uint32_t voicePromptsFlashDataAddress;// = VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t) + sizeof(uint32_t)*VOICE_PROMPTS_TOC_SIZE ;
// 76 x 27 byte ambe frames
#define AMBE_DATA_BUFFER_SIZE  2052
bool voicePromptDataIsLoaded = false;
static volatile bool voicePromptIsActive = false; // used within ISR
static int promptDataPosition = -1;
static int currentPromptLength = -1;

#define PROMPT_TAIL  30
static volatile uint32_t promptTail = 0; // used within ISR

static __attribute__((section(".ccmram"))) uint8_t ambeData[AMBE_DATA_BUFFER_SIZE];

#define VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE 128

typedef struct
{
	uint16_t  Buffer[VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE];
	uint32_t  Pos;
	uint32_t  Length;
} VoicePromptsSequence_t;

static __attribute__((section(".ccmram"))) VoicePromptsSequence_t voicePromptsCurrentSequence =
{
	.Pos = 0,
	.Length = 0
};

static __attribute__((section(".ccmram"))) uint32_t tableOfContents[VOICE_PROMPTS_TOC_SIZE];

static bool temporaryOverride = false;

void voicePromptsCacheInit(void)
{
	VoicePromptsDataHeader_t header;

	SPI_Flash_read(VOICE_PROMPTS_FLASH_HEADER_ADDRESS, (uint8_t *)&header, sizeof(VoicePromptsDataHeader_t));

	if (voicePromptsCheckMagicAndVersion((uint32_t *)&header))
	{
		voicePromptDataIsLoaded = SPI_Flash_read(VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t), (uint8_t *)&tableOfContents, sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
		voicePromptsFlashDataAddress = VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t) + (sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
	}

	if (!voicePromptDataIsLoaded)
	{
		SPI_Flash_read(VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS, (uint8_t *)&header, sizeof(VoicePromptsDataHeader_t));
		if (voicePromptsCheckMagicAndVersion((uint32_t *)&header))
		{
			voicePromptDataIsLoaded = SPI_Flash_read(VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t), (uint8_t *)&tableOfContents, sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
			voicePromptsFlashDataAddress = VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t) + (sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
		}
	}

	// DMR (digital) is disabled.
	if (uiDataGlobal.dmrDisabled)
	{
		voicePromptDataIsLoaded = false;
	}

	// is data is not loaded change prompt mode back to beep.
	if ((nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && (voicePromptDataIsLoaded == false))
	{
		settingsSet(nonVolatileSettings.audioPromptMode, AUDIO_PROMPT_MODE_BEEP);
	}
}

bool voicePromptsCheckMagicAndVersion(uint32_t *bufferAddress)
{
	VoicePromptsDataHeader_t *header = (VoicePromptsDataHeader_t *)bufferAddress;

	return ((header->magic == VOICE_PROMPTS_DATA_MAGIC) && (header->version == VOICE_PROMPTS_DATA_VERSION));
}

static void getAmbeData(int offset, int length)
{
	if (length <= AMBE_DATA_BUFFER_SIZE)
	{
		SPI_Flash_read(voicePromptsFlashDataAddress + offset, (uint8_t *)&ambeData, length);
	}
}

static void voicePromptsTerminateOptionalTail(bool withTail)
{
	if (voicePromptIsActive)
	{
		audioAmpDisable(AUDIO_AMP_CHANNEL_PROMPT);

		voicePromptsCurrentSequence.Pos = 0;
		promptTail = (withTail ? PROMPT_TAIL : 0);

		taskENTER_CRITICAL();
		soundTerminateSound();
		codecInit(true);
		voicePromptIsActive = false;
		temporaryOverride = false;
		taskEXIT_CRITICAL();
	}
}

void voicePromptsTick(void)
{
	if (voicePromptIsActive)
	{
		if (promptDataPosition < currentPromptLength)
		{
			taskENTER_CRITICAL();
			if (wavbuffer_count <= WAV_BUFFER_AMBE_PREBUFFERING_COUNT)
			{
				codecDecode((uint8_t *)&ambeData[promptDataPosition], 3);
				promptDataPosition += AMBE_AUDIO_LENGTH;
			}

			soundTickRXBuffer();
			taskEXIT_CRITICAL();
		}
		else
		{
			if (voicePromptsCurrentSequence.Pos < (voicePromptsCurrentSequence.Length - 1))
			{
				voicePromptsCurrentSequence.Pos++;
				promptDataPosition = 0;

				int promptNumber = voicePromptsCurrentSequence.Buffer[voicePromptsCurrentSequence.Pos];

				if ((tableOfContents[promptNumber + 1] == 0) || (tableOfContents[promptNumber] == 0))
				{
					promptNumber = PROMPT_SILENCE;
				}

				currentPromptLength = tableOfContents[promptNumber + 1] - tableOfContents[promptNumber];
				getAmbeData(tableOfContents[promptNumber], currentPromptLength);
			}
			else
			{
				// wait for wave buffer to empty when prompt has finished playing

				if (wavbuffer_count == 0)
				{
					voicePromptsTerminateOptionalTail(true);
				}
			}
		}
	}
	else
	{
		if (promptTail > 0)
		{
			promptTail--;

			if ((promptTail == 0) && trxCarrierDetected(RADIO_DEVICE_PRIMARY) && (trxGetMode() == RADIO_MODE_ANALOG))
			{
				radioSetAudioPath(true);					// Set the audio path to FM -> audio amp.
			}
		}
	}
}

void voicePromptsTerminate(void)
{
	voicePromptsTerminateOptionalTail(true);
}

void voicePromptsTerminateNoTail(void)
{
	voicePromptsTerminateOptionalTail(false);
}

void voicePromptsInit(void)
{
	if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
	{
		return;
	}

	if (voicePromptIsActive)
	{
		voicePromptsTerminateOptionalTail(false);
	}

	voicePromptsCurrentSequence.Length = 0;
	voicePromptsCurrentSequence.Pos = 0;
}

void voicePromptsInitWithOverride(void)
{
	temporaryOverride = voicePromptDataIsLoaded;// only allow override if prompts are actually loaded
	voicePromptsInit();
}

void voicePromptsAppendPrompt(voicePrompt_t prompt)
{
	if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
	{
		return;
	}

	if (voicePromptIsActive)
	{
		voicePromptsInit();
	}

	if (voicePromptsCurrentSequence.Length < VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE)
	{
		voicePromptsCurrentSequence.Buffer[voicePromptsCurrentSequence.Length] = prompt;
		voicePromptsCurrentSequence.Length++;
	}
}

void voicePromptsAppendString(const char *promptString)
{
	if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
	{
		return;
	}

	if (voicePromptIsActive)
	{
		voicePromptsInit();
	}

	while (*promptString != 0)
	{
		if ((*promptString >= '0') && (*promptString <= '9'))
		{
			voicePromptsAppendPrompt(*promptString - '0' + PROMPT_0);
		}
		else if ((*promptString >= 'A') && (*promptString <= 'Z'))
		{
			voicePromptsAppendPrompt(*promptString - 'A' + PROMPT_A);
		}
		else if ((*promptString >= 'a') && (*promptString <= 'z'))
		{
			voicePromptsAppendPrompt(*promptString - 'a' + PROMPT_A);
		}
		else if (*promptString == '.')
		{
			voicePromptsAppendPrompt(PROMPT_POINT);
		}
		else if (*promptString == '+')
		{
			voicePromptsAppendPrompt(PROMPT_PLUS);
		}
		else if (*promptString == '-')
		{
			voicePromptsAppendPrompt(PROMPT_MINUS);
		}
		else if (*promptString == '%')
		{
			voicePromptsAppendPrompt(PROMPT_PERCENT);
		}
		else if (*promptString == '*')
		{
			voicePromptsAppendPrompt(PROMPT_STAR);
		}
		else if (*promptString == '#')
		{
			voicePromptsAppendPrompt(PROMPT_HASH);
		}
		else if (*promptString == 176)
		{
			voicePromptsAppendPrompt(PROMPT_DEGREES);
		}
		else
		{
			// otherwise just add silence
			voicePromptsAppendPrompt(PROMPT_SILENCE);
		}

		promptString++;
	}
}

void voicePromptsAppendInteger(int32_t value)
{
	char buf[12] = {0}; // min: -2147483648, max: 2147483647
	itoa(value, buf, 10);
	voicePromptsAppendString(buf);
}

void voicePromptsAppendLanguageString(const char *languageStringAdd)
{
	if (((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride) || (languageStringAdd == NULL))
	{
		return;
	}

	// The prompt index is derived from the pointer's offset within the currentLanguage
	// table, so a pointer that isn't actually inside that table (e.g. a raw string
	// literal passed by mistake) would yield an out-of-range prompt and crash. Ignore it.
	const char *languageBase = &currentLanguage->LANGUAGE_NAME[0];
	if ((languageStringAdd < languageBase) || (languageStringAdd >= (languageBase + sizeof(*currentLanguage))))
	{
		return;
	}

	int promptNumber = NUM_VOICE_PROMPTS +
			((languageStringAdd - currentLanguage->LANGUAGE_NAME)
#if ! defined(HAS_COLOURS)
					- ((languageStringAdd >= currentLanguage->theme_chooser) ?
							((currentLanguage->theme_colour_picker_blue - currentLanguage->theme_chooser) + LANGUAGE_TEXTS_LENGTH) : 0)
#endif
			) / LANGUAGE_TEXTS_LENGTH;

	// Language strings added after the voice prompt pack was built have no
	// entry in the table of contents. voicePromptsPlay() reads TOC[promptNumber + 1],
	// so anything at or past the last slot would read past the array and crash;
	// play silence for those instead.
	if ((promptNumber < 0) || (promptNumber >= (VOICE_PROMPTS_TOC_SIZE - 1)))
	{
		voicePromptsAppendPrompt(PROMPT_SILENCE);
		return;
	}

	voicePromptsAppendPrompt(promptNumber);
}

void voicePromptsPlay(void)
{
	if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
	{
		return;
	}

	if ((voicePromptIsActive == false) && (voicePromptsCurrentSequence.Length > 0))
	{
		rxPowerSavingSetState(ECOPHASE_POWERSAVE_INACTIVE);

		if (nonVolatileSettings.dmrRxAGC != 0)
		{
			soundResetDMRRxAGCGain();
		}

		taskENTER_CRITICAL();

		// Early state change to lock out HR-C6000.
		voicePromptIsActive = true;// Start the playback
		if (melody_play && (melody_play != MELODY_ERROR_BEEP) && (melody_play != MELODY_ACK_BEEP))
		{
			soundStopMelody();
		}

		int promptNumber = voicePromptsCurrentSequence.Buffer[0];

		voicePromptsCurrentSequence.Pos = 0;

		if ((tableOfContents[promptNumber + 1] == 0) || (tableOfContents[promptNumber] == 0))
		{
			promptNumber = PROMPT_SILENCE;
		}

		currentPromptLength = tableOfContents[promptNumber + 1] - tableOfContents[promptNumber];
		getAmbeData(tableOfContents[promptNumber], currentPromptLength);

		radioSetAudioPath(false);			// set the audio path to HR-C6000 -> audio amp (Actually this is always on, this call just disables the FM audio)
		audioAmpEnable(AUDIO_AMP_CHANNEL_PROMPT);

		codecInit(true);
		promptDataPosition = 0;
		promptTail = 0;

		taskEXIT_CRITICAL();
	}
}

inline bool voicePromptsIsPlaying(void)
{
	return (voicePromptIsActive || (promptTail > 0));
}

bool voicePromptsHasDataToPlay(void)
{
	return (voicePromptsCurrentSequence.Length > 0);
}

bool voicePromptsDoesItContainPrompt(voicePrompt_t prompt)
{
	if (voicePromptsCurrentSequence.Length > 0)
	{
		return (memchr(&voicePromptsCurrentSequence.Buffer[0], (int)prompt, voicePromptsCurrentSequence.Length) != NULL);
	}
	return false;
}
