"""Defines core voice interfaces and compatibility exports."""

import logging
from types import TracebackType
from typing import Dict, List, Optional, Tuple, Type

import sqlalchemy

from .voice_audio import Audio
from .voice_db import (
    DB_NAME,
    DatabaseConnection,
    DatabaseInsertFailed,
    NoDatabaseSpecified,
    SentenceIdNotFound,
    create_database_connection,
    get_generated_sentence_from_id,
    get_generated_sentences_dict,
    get_generated_sentences_list,
    insert_sentence_into_db,
    sentence_exists,
)
from .voice_modifiers import (
    MODIFIERS,
    AcceleratorDirection,
    AcceleratorDirectionInvalid,
    AcceleratorModifier,
    CutModifier,
    Modifier,
    ModifierArgumentsInvalid,
    ModifierClass,
    ModifierFormatNotCorrect,
    Modifiers,
    NoOpModifier1,
    NoOpModifier2,
    PitchChangeModifier,
    SpeedChangeModifier,
    VolumeChangeModifier,
)
from .voice_sentence import (
    NoVoiceSpecified,
    PUNCTUATION_TIMING_SECONDS,
    Sentence,
    Word,
    extract_modifiers,
    process_sentence,
    split_sentence,
)

log = logging.getLogger(__name__)

__all__ = [
    "Audio",
    "DB_NAME",
    "DatabaseConnection",
    "DatabaseInsertFailed",
    "NoDatabaseSpecified",
    "SentenceIdNotFound",
    "MODIFIERS",
    "AcceleratorDirection",
    "AcceleratorDirectionInvalid",
    "AcceleratorModifier",
    "CutModifier",
    "Modifier",
    "ModifierArgumentsInvalid",
    "ModifierClass",
    "ModifierFormatNotCorrect",
    "Modifiers",
    "NoOpModifier1",
    "NoOpModifier2",
    "PitchChangeModifier",
    "SpeedChangeModifier",
    "VolumeChangeModifier",
    "NoVoiceSpecified",
    "PUNCTUATION_TIMING_SECONDS",
    "Sentence",
    "Word",
    "NoWordsFound",
    "DuplicateWords",
    "InconsistentAudioFormats",
    "NoAudioFormatFound",
    "FailedToSplit",
    "ModifierSyntaxError",
    "Voice",
]


class NoWordsFound(Exception):
    """Raised when a voice has no words."""


class DuplicateWords(Exception):
    """Raised when a voice has duplicate words."""


class InconsistentAudioFormats(Exception):
    """Raised when words have inconsistent audio formats."""


class NoAudioFormatFound(Exception):
    """Raised when no audio format can be found."""


class FailedToSplit(Exception):
    """Raised when a sentence cannot be split."""


class ModifierSyntaxError(Exception):
    """Raised when there is a problem with the modifier syntax."""


class Voice:
    """Base class for Voice-like interfaces.
    Intended to involve generation of audio files from some source (files, web, etc).
    """

    def __init__(
        self,
        name: str,
        database: Optional[sqlalchemy.engine.Engine],
    ) -> None:
        self.name = name
        self._db = create_database_connection(voice_name=self.name, database=database)
        self.words: List[Word] = []
        self.categories: Dict[str, List[str]] = {}

    def __enter__(self) -> "Voice":
        return self

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> None:
        self.exit()

    def exit(self) -> None:
        """Clean up and close voice."""
        if self._db is not None:
            self._db.engine.dispose()

    def _insert_sentence_into_db(self, sentence: str) -> int:
        return insert_sentence_into_db(db=self._db, sentence=sentence)

    def _sentence_exists(self, sentence: str) -> bool:
        return sentence_exists(db=self._db, sentence=sentence)

    def _get_generated_sentences_list(self) -> list[str]:
        return get_generated_sentences_list(db=self._db)

    def _get_generated_sentences_dict(self) -> dict[int, str]:
        return get_generated_sentences_dict(db=self._db)

    def _get_generated_sentence_from_id(self, sentence_id: str) -> Optional[str]:
        return get_generated_sentence_from_id(db=self._db, sentence_id=sentence_id)

    def _get_sentence_info(self, words: List[Word]) -> Sentence:
        sayable_words, unsayable_words = self.get_sayable_unsayable(words)
        sayable_sent_arr = self._get_sayable_sentence_arr(words, sayable_words)
        sayable_sent_str = self._create_sentence_string(sayable_sent_arr)

        return Sentence(
            sentence=sayable_sent_str,
            sayable=sayable_words,
            unsayable=unsayable_words,
            sayable_sentence=sayable_sent_arr,
            audio=None,
            id=None,
        )

    def generate_audio_from_array(
        self, words: List[Word], dry_run: bool = False, save_to_db: bool = True
    ) -> Sentence:
        """Generates audio segment from sentence array."""
        sentence_info = self._get_sentence_info(words=words)

        if dry_run:
            return sentence_info

        log.debug("Generating %s", sentence_info.sentence)

        if len(sentence_info.sayable) == 0:
            log.warning(
                "Can't say any words in %s, not generating", sentence_info.sentence
            )
            return sentence_info

        if self._db and save_to_db and len(words) > 1:
            if not self._sentence_exists(sentence=sentence_info.sentence):
                sentence_id = self._insert_sentence_into_db(
                    sentence=sentence_info.sentence
                )
                sentence_info.id = str(sentence_id)

        words_audio = self._create_audio_segments(sentence_info.sayable_sentence)
        sentence_info.audio = self.assemble_audio_segments(words_audio)

        return sentence_info

    def _create_audio_segments(
        self,
        word_array: List[Word],  # pylint: disable=unused-argument
    ) -> List[Audio]:
        return []

    def generate_audio(self, sentence: str, dry_run: bool = False) -> Sentence:
        """Generates audio from the given sentence."""
        log.info("Asked to generate %s", sentence)
        split = self._split_sentence(sentence)
        processed_sentence = self.process_sentence(split, voice=self.name)
        return self.generate_audio_from_array(words=processed_sentence, dry_run=dry_run)

    def generate_audio_from_id(
        self, sentence_id: str, dry_run: bool = False
    ) -> Sentence:
        """Generate audio from an existing sentence, fetched by its ID."""
        log.info("Fetching audio for ID %s", sentence_id)
        sentence = self._get_generated_sentence_from_id(sentence_id)
        if not sentence:
            raise SentenceIdNotFound
        return self.generate_audio(sentence=sentence, dry_run=dry_run)

    @staticmethod
    def _split_sentence(sentence: str) -> List[Word]:
        return split_sentence(sentence)

    @staticmethod
    def _extract_modifiers(words: List[Word]) -> List[Word]:
        return extract_modifiers(words)

    @staticmethod
    def process_sentence(split_sent: List[Word], voice: Optional[str]) -> List[Word]:
        return process_sentence(split_sent=split_sent, voice=voice)

    def get_sayable_unsayable(self, words: List[Word]) -> Tuple[List[Word], List[Word]]:
        """Get words that are sayable or unsayable from a list of words."""
        sayable_words_set = set(self.words)
        sayable_words_set.update(
            [
                Word(word=punct, voice=self.name, is_punctuation=True)
                for punct in PUNCTUATION_TIMING_SECONDS
            ]
        )

        no_modifier_words = [word.without_modifiers() for word in words]
        words_set = set((dict.fromkeys(no_modifier_words)))

        unsayable_set = words_set - sayable_words_set
        sayable_set = words_set - unsayable_set
        unsayable = list(unsayable_set)
        unsayable.sort()
        sayable = list(sayable_set)
        sayable.sort()
        return sayable, unsayable

    def _get_sayable_sentence_arr(
        self, words: List[Word], sayable_words: List[Word]
    ) -> List[Word]:
        return [
            word
            for word in words
            if Word(
                word=word.word, voice=word.voice, is_punctuation=word.is_punctuation
            )
            in sayable_words
        ]

    def _create_sentence_string(self, words: List[Word]) -> str:
        if len(words) == 1:
            return words[0].as_str(with_voice=False)
        return " ".join([word.as_str(with_voice=False) for word in words])

    def get_generated_sentences(self) -> List[str]:
        return self._get_generated_sentences_list()

    def get_generated_sentences_dict(self) -> Dict[int, str]:
        return self._get_generated_sentences_dict()

    @staticmethod
    def assemble_audio_segments(segments: List[Audio]) -> Audio:
        """Assemble audio segments into one audio segment."""
        frame_rates = [word.get_sample_rate() for word in segments]
        frame_rate = min(frame_rates)

        sentence_audio = segments.pop(0)
        if sentence_audio.get_sample_rate() != frame_rate:
            sentence_audio.set_sample_rate(frame_rate)

        for word_audio in segments:
            if word_audio.get_sample_rate() != frame_rate:
                word_audio.set_sample_rate(frame_rate)
            sentence_audio = sentence_audio + word_audio

        return sentence_audio
