#!/usr/bin/env python3
"""Language-specific settings"""
import logging
import re
import sqlite3
import typing
from pathlib import Path

import networkx as nx

from gruut.const import PHONEMES_TYPE, GraphType, SentenceNode, Time
from gruut.g2p import GraphemesToPhonemes
from gruut.phonemize import SqlitePhonemizer
from gruut.pos import PartOfSpeechTagger
from gruut.text_processor import InterpretAsFormat, TextProcessorSettings
from gruut.utils import find_lang_dir, remove_non_word_chars, resolve_lang

_LOGGER = logging.getLogger("gruut")

# -----------------------------------------------------------------------------


def get_settings(
    lang: str,
    search_dirs: typing.Optional[typing.Iterable[typing.Union[str, Path]]] = None,
    lang_dir: typing.Optional[typing.Union[str, Path]] = None,
    model_prefix: typing.Optional[str] = None,
    load_pos_tagger: bool = True,
    load_phoneme_lexicon: bool = True,
    load_g2p_guesser: bool = True,
    **settings_args,
) -> TextProcessorSettings:
    """Get settings for a specific language"""
    model_prefix = model_prefix or ""

    # Resolve language
    if model_prefix:
        # espeak
        lang_model_prefix = model_prefix
        lang_only = lang
    elif "/" in lang:
        # en-us/espeak
        lang_only, lang_model_prefix = lang.split("/", maxsplit=1)
    else:
        # en-us
        lang_only = lang
        lang_model_prefix = ""

    # en_US -> en-us
    lang_only = resolve_lang(lang_only)

    if lang_dir is None:
        # Search for language data files
        lang_dir = find_lang_dir(lang_only, search_dirs=search_dirs)

    if lang_dir is not None:
        lang_dir = Path(lang_dir)

        # Part of speech tagger
        if load_pos_tagger and ("get_parts_of_speech" not in settings_args):
            pos_model_path = lang_dir / "pos" / "model.crf"
            if pos_model_path.is_file():
                # POS tagger model will load on first use
                settings_args["get_parts_of_speech"] = DelayedPartOfSpeechTagger(
                    pos_model_path
                )
            else:
                _LOGGER.debug(
                    "(%s) no part of speech tagger found at %s", lang, pos_model_path,
                )

        # Phonemizer
        if load_phoneme_lexicon and ("lookup_phonemes" not in settings_args):
            lexicon_db_path = lang_dir / lang_model_prefix / "lexicon.db"
            if lexicon_db_path.is_file():
                # Transformations to apply to words when they can't be found in the lexicon
                phonemizer_args = {
                    "word_transform_funcs": [
                        remove_non_word_chars,
                        lambda s: remove_non_word_chars(s.lower()),
                    ],
                    "casing_func": str.lower,
                }

                settings_args["lookup_phonemes"] = DelayedSqlitePhonemizer(
                    lexicon_db_path, **phonemizer_args
                )
            else:
                _LOGGER.debug(
                    "(%s) no phoneme lexicon database found at %s",
                    lang,
                    lexicon_db_path,
                )

        # Grapheme to phoneme model
        if load_g2p_guesser and ("guess_phonemes" not in settings_args):
            g2p_model_path = lang_dir / lang_model_prefix / "g2p" / "model.crf"
            if g2p_model_path.is_file():
                settings_args["guess_phonemes"] = DelayedGraphemesToPhonemes(
                    g2p_model_path, transform_func=str.lower
                )

            else:
                _LOGGER.debug(
                    "(%s) no grapheme to phoneme CRF model found at %s",
                    lang,
                    g2p_model_path,
                )

    # ---------------------------------
    # Create language-specific settings
    # ---------------------------------

    if lang_only == "ar":
        # Arabic
        return get_ar_settings(lang_dir, **settings_args)

    if lang_only == "cs-cz":
        # Czech
        return get_cs_settings(lang_dir, **settings_args)

    if lang_only in {"en-us", "en-gb"}:
        # English
        return get_en_us_settings(lang_dir, **settings_args)

    if lang_only == "de-de":
        # German
        return get_de_settings(lang_dir, **settings_args)

    if lang_only in {"es-es", "es-mx"}:
        # Spanish
        return get_es_settings(lang_dir, **settings_args)

    if lang_only == "fa":
        # Farsi
        return get_fa_settings(lang_dir, **settings_args)

    if lang_only == "fr-fr":
        # French
        return get_fr_settings(lang_dir, **settings_args)

    if lang_only == "it-it":
        # Italian
        return get_it_settings(lang_dir, **settings_args)

    if lang_only == "lb":
        # Lëtzebuergesch
        return get_lb_settings(lang_dir, **settings_args)

    if lang_only == "nl":
        # Dutch
        return get_nl_settings(lang_dir, **settings_args)

    if lang_only == "pt":
        # Portuguese
        return get_pt_settings(lang_dir, **settings_args)

    if lang_only == "ru-ru":
        # Russian
        return get_ru_settings(lang_dir, **settings_args)

    if lang_only == "sv-se":
        # Swedish
        return get_sv_settings(lang_dir, **settings_args)

    if lang_only == "sw":
        # Swahili
        return get_sw_settings(lang_dir, **settings_args)

    if lang_only == "zh-cn":
        # Chinese
        return get_zh_settings(lang_dir, **settings_args)

    # Default settings only
    return TextProcessorSettings(lang=lang, **settings_args)


# -----------------------------------------------------------------------------
# Arabic (ar, اَلْعَرَبِيَّةُ)
# -----------------------------------------------------------------------------


class ArabicPreProcessText:
    """Pre-processes text using mishkal"""

    def __call__(self, text: str) -> str:
        try:
            import mishkal.tashkeel

            # Load vocalizer
            if not hasattr(self, "vocalizer"):
                vocalizer = mishkal.tashkeel.TashkeelClass()
                setattr(self, "vocalizer", vocalizer)
            else:
                vocalizer = getattr(self, "vocalizer")

            assert vocalizer is not None

            # Add diacritics
            text = vocalizer.tashkeel(text)
        except ImportError:
            _LOGGER.warning("mishkal is highly recommended for language 'ar'")
            _LOGGER.warning("pip install 'mishkal>=0.4.0'")

        return text


def get_ar_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Arabic"""
    settings_args = {
        "major_breaks": {".", "؟", "!"},
        "minor_breaks": {"،", ";", ":"},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        "pre_process_text": ArabicPreProcessText(),
        **settings_args,
    }
    return TextProcessorSettings(lang="ar", **settings_args)


# -----------------------------------------------------------------------------
# Czech (cs-cz, čeština)
# -----------------------------------------------------------------------------


def get_cs_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Czech"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":"},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "’", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">", "’"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="cs_CZ", **settings_args)


# -----------------------------------------------------------------------------
# English (en-us, en-gb)
# -----------------------------------------------------------------------------


# TTS and T.T.S.
EN_INITIALISM_PATTERN = re.compile(r"^\s*[A-Z]{2,}\s*$")
EN_INITIALISM_DOTS_PATTERN = re.compile(r"^(?:\s*[a-zA-Z]\.){1,}\s*$")

EN_NON_WORD_PATTERN = re.compile(r"^(\W|_)+$")
EN_ORDINAL_PATTERN = re.compile(r"^(-?[0-9][0-9,]*)(?:st|nd|rd|th).*$")

EN_TIME_PATTERN = re.compile(
    r"""^((0?[0-9])|(1[0-1])|(1[2-9])|(2[0-3]))  # hours
         (?::
         ([0-5][0-9]))?                          # minutes
         \s*(a\\.m\\.|am|pm|p\\.m\\.|a\\.m|p\\.m)? # am/pm
         $""",
    re.IGNORECASE | re.X,
)


def en_is_initialism(text: str) -> bool:
    """True if text is of the form TTS or T.T.S."""
    return (EN_INITIALISM_PATTERN.match(text) is not None) or (
        EN_INITIALISM_DOTS_PATTERN.match(text) is not None
    )


def en_get_ordinal(text: str) -> typing.Optional[int]:
    """Parse English ordinal string (e.g., 1st -> 1)"""
    match = EN_ORDINAL_PATTERN.match(text)
    if match is not None:
        return int(re.sub(r"[^0-9]", "", match.group(1)))

    return None


def en_parse_time(text: str) -> typing.Optional[Time]:
    """Parse English clock time (e.g. 4:01pm)"""
    match = EN_TIME_PATTERN.match(text.strip().lower())
    if match is None:
        return None

    hours = int(match.group(1))
    maybe_minutes = match.group(6)
    minutes = 0 if maybe_minutes is None else int(maybe_minutes)
    period = match.group(7)

    if period is not None:
        # Normalize period
        if "a" in period:
            period = "A.M."
        else:
            period = "P.M."
    else:
        if ":" not in text:
            # Require a colon if no period is specified to avoid parsing plain
            # numbers like "1" into time expressions.
            return None

    return Time(hours=hours, minutes=minutes, period=period)


def en_verbalize_time(time: Time) -> typing.Iterable[str]:
    """Convert time into words"""

    hour = time.hours
    past_noon = hour >= 12

    if hour > 12:
        hour -= 12
    elif hour == 0:
        hour = 12
        past_noon = True

    yield str(hour)

    minute = time.minutes
    if minute > 0:
        if minute < 10:
            yield "oh"

        yield str(minute)

    if time.period is None:
        if past_noon:
            yield "P.M."
        else:
            yield "A.M."
    else:
        yield time.period


def get_en_us_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for English"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "'", "“", "«", "[", "(", "<", "*", "_"},
        "end_punctuations": {'"', "'", "”", "»", "]", ")", ">", "*", "_"},
        "default_currency": "USD",
        "default_date_format": "{m} {o}, {y}",
        "is_initialism": en_is_initialism,
        "split_initialism": lambda text: list(text.replace(".", "")),
        "is_non_word": lambda text: EN_NON_WORD_PATTERN.match(text) is not None,
        "get_ordinal": en_get_ordinal,
        "parse_time": en_parse_time,
        "verbalize_time": en_verbalize_time,
        "replacements": [("’", "'")],  # normalize apostrophe
        "abbreviations": {
            r"^([cC])o\.": r"\1ompany",  # co. -> company
            r"^([dD])r\.": r"\1octor",  # dr. -> doctor
            r"^([dD])rs\.": r"\1octors",  # drs. -> doctors
            r"^([jJ])r\.('s)?": r"\1unior\2",  # jr. -> junior
            r"^([lL])td\.": r"\1imited",  # -> ltd. -> limited
            r"^([mM])r\.": r"\1ister",  # -> mr. -> mister
            r"^([mM])s\.": r"\1iss",  # -> ms. -> miss
            r"^([mM])rs\.": r"\1issus",  # -> mrs. -> missus
            r"^([sS])t\.": r"\1treet",  # -> st. -> street
            r"^([vV])s\.?": r"\1ersus",  # -> vs. -> versus
            r"(.*\d)%": r"\1 percent",  # % -> percent
            r"^&(\s*)$": r"and\1",  # &-> and
            r"^([mM])t\.": r"\1ount",  # -> mt. -> mount
        },
        "spell_out_words": {
            ".": "dot",
            "-": "dash",
            "@": "at",
            "*": "star",
            "+": "plus",
            "/": "slash",
        },
        **settings_args,
    }

    return TextProcessorSettings(lang="en_US", **settings_args)


# -----------------------------------------------------------------------------
# German (de-de)
# -----------------------------------------------------------------------------


def get_de_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for German"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "’", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">", "’"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY_ORDINAL,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="de_DE", **settings_args)


# -----------------------------------------------------------------------------
# Spanish (es-es, Español)
# -----------------------------------------------------------------------------


def get_es_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Spanish"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "¡", "¿"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="es_ES", **settings_args)


# -----------------------------------------------------------------------------
# Farsi/Persian (fa, فارسی)
# -----------------------------------------------------------------------------


class FarsiPartOfSpeechTagger:
    """Add POS tags with hazm"""

    def __init__(self, lang_dir: Path):
        self.lang_dir = lang_dir

    def __call__(self, words: typing.Sequence[str]) -> typing.Sequence[str]:
        pos_tags = []

        try:
            import hazm

            # Load normalizer
            normalizer = getattr(self, "normalizer", None)
            if normalizer is None:
                normalizer = hazm.Normalizer()
                setattr(self, "normalizer", normalizer)

            # Load tagger
            tagger = getattr(self, "tagger", None)
            if tagger is None:
                # Load part of speech tagger
                model_path = self.lang_dir / "pos" / "postagger.model"
                tagger = hazm.POSTagger(model=str(model_path))
                setattr(self, "tagger", tagger)

            text = " ".join(words)
            for sentence in hazm.sent_tokenize(normalizer.normalize(text)):
                for _word, pos in tagger.tag(hazm.word_tokenize(sentence)):
                    pos_tags.append(pos)
        except ImportError:
            _LOGGER.warning("hazm is highly recommended for language 'fa'")
            _LOGGER.warning("pip install 'hazm>=0.7.0'")

        return pos_tags


def fa_post_process_sentence(
    graph: GraphType, sent_node: SentenceNode, settings: TextProcessorSettings
):
    """Add e̞ for genitive case"""
    from gruut.text_processor import DATA_PROP, WordNode

    for dfs_node in nx.dfs_preorder_nodes(graph, sent_node.node):
        if not graph.out_degree(dfs_node) == 0:
            # Only leave
            continue

        node = graph.nodes[dfs_node][DATA_PROP]
        if isinstance(node, WordNode):
            word = typing.cast(WordNode, node)
            if word.phonemes and (word.pos == "Ne"):
                if isinstance(word.phonemes, list):
                    word.phonemes.append("e̞")
                else:
                    word.phonemes = list(word.phonemes) + ["e̞"]


def get_fa_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Farsi"""
    settings_args = {
        "major_breaks": {".", "؟", "!"},
        "minor_breaks": {",", ";", ":"},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "’", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">", "’"},
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        "post_process_sentence": fa_post_process_sentence,
        **settings_args,
    }

    if (lang_dir is not None) and ("get_parts_of_speech" not in settings_args):
        settings_args["get_parts_of_speech"] = FarsiPartOfSpeechTagger(lang_dir)

    return TextProcessorSettings(lang="fa", **settings_args)


# -----------------------------------------------------------------------------
# French (fr-fr, Français)
# -----------------------------------------------------------------------------


def fr_post_process_sentence(
    graph: GraphType, sent_node: SentenceNode, settings: TextProcessorSettings
):
    """Add liasons to phonemes"""
    from gruut.text_processor import DATA_PROP, WordNode
    from gruut.utils import sliding_window

    words = []
    for dfs_node in nx.dfs_preorder_nodes(graph, sent_node.node):
        if not graph.out_degree(dfs_node) == 0:
            # Only leave
            continue

        node = graph.nodes[dfs_node][DATA_PROP]
        if isinstance(node, WordNode):
            word_node = typing.cast(WordNode, node)
            words.append(word_node)

    for word1, word2 in sliding_window(words, 2):
        if word2 is None:
            continue

        if not (word1.text and word1.phonemes and word2.text and word2.phonemes):
            continue

        liason = False

        # Conditions to meet for liason check:
        # 1) word 1 ends with a silent consonant
        # 2) word 2 starts with a vowel (phoneme)

        last_char1 = word1.text[-1]
        ends_silent_consonant = fr_has_silent_consonant(last_char1, word1.phonemes[-1])
        starts_vowel = fr_is_vowel(word2.phonemes[0])

        if ends_silent_consonant and starts_vowel:
            # Handle mandatory liason cases
            # https://www.commeunefrancaise.com/blog/la-liaison

            if word1.text == "et":
                # No liason
                pass
            elif word1.pos in {"DET", "NUM"}:
                # Determiner/adjective -> noun
                liason = True
            elif (word1.pos == "PRON") and (word2.pos in {"AUX", "VERB"}):
                # Pronoun -> verb
                liason = True
            elif (word1.pos == "ADP") or (word1.text == "très"):
                # Preposition
                liason = True
            elif (word1.pos == "ADJ") and (word2.pos in {"NOUN", "PROPN"}):
                # Adjective -> noun
                liason = True
            elif word1.pos in {"AUX", "VERB"}:
                # Verb -> vowel
                liason = True

        if liason:
            # Apply liason
            # s -> z
            # p -> p
            # d|t -> d
            liason_pron = word1.phonemes

            if last_char1 in {"s", "x", "z"}:
                liason_pron.append("z")
            elif last_char1 == "d":
                liason_pron.append("t")
            elif last_char1 in {"t", "p", "n"}:
                # Final phoneme is same as char
                liason_pron.append(last_char1)


def fr_has_silent_consonant(last_char: str, last_phoneme: str) -> bool:
    """True if last consonant is silent in French"""
    # Credit: https://github.com/Remiphilius/PoemesProfonds/blob/master/lecture.py

    if last_char in {"d", "p", "t"}:
        return last_phoneme != last_char
    if last_char == "r":
        return last_phoneme != "ʁ"
    if last_char in {"s", "x", "z"}:
        return last_phoneme not in {"s", "z"}
    if last_char == "n":
        return last_phoneme not in {"n", "ŋ"}

    return False


def fr_is_vowel(phoneme: str) -> bool:
    """True if phoneme is a French vowel"""
    return phoneme in {
        "i",
        "y",
        "u",
        "e",
        "ø",
        "o",
        "ə",
        "ɛ",
        "œ",
        "ɔ",
        "a",
        "ɔ̃",
        "ɛ̃",
        "ɑ̃",
        "œ̃",
    }


def get_fr_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for French"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY_ORDINAL,
        "replacements": [("’", "'")],  # normalize apostrophe
        "post_process_sentence": fr_post_process_sentence,
        **settings_args,
    }
    return TextProcessorSettings(lang="fr_FR", **settings_args)


# -----------------------------------------------------------------------------
# Italian (it-it, Italiano)
# -----------------------------------------------------------------------------


def get_it_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Italian"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        "post_process_sentence": fr_post_process_sentence,
        **settings_args,
    }
    return TextProcessorSettings(lang="it_IT", **settings_args)


# -----------------------------------------------------------------------------
# Luxembourgish (lb, Lëtzebuergesch)
# -----------------------------------------------------------------------------


def get_lb_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Luxembourgish"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        "babel_locale": "lb",
        **settings_args,
    }
    return TextProcessorSettings(lang="lb", **settings_args)


# -----------------------------------------------------------------------------
# Dutch (nl, Nederlands)
# -----------------------------------------------------------------------------


def get_nl_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Dutch"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="nl", **settings_args)


# -----------------------------------------------------------------------------
# Portuguese (pt, Português)
# -----------------------------------------------------------------------------


def get_pt_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create default settings for Portuguese"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "EUR",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="pt", **settings_args)


# -----------------------------------------------------------------------------
# Russian (ru, Русский)
# -----------------------------------------------------------------------------


def get_ru_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Russian"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":"},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_currency": "RUB",
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="ru_RU", **settings_args)


# -----------------------------------------------------------------------------
# Swedish (sv-se, svenska)
# -----------------------------------------------------------------------------


def get_sv_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Swedish"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":", "..."},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="sv_SE", **settings_args)


# -----------------------------------------------------------------------------
# Swahili (sw, Kiswahili)
# -----------------------------------------------------------------------------


def get_sw_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Swahili"""
    settings_args = {
        "major_breaks": {".", "?", "!"},
        "minor_breaks": {",", ";", ":"},
        "word_breaks": {"-", "_"},
        "begin_punctuations": {'"', "“", "«", "[", "(", "<", "„"},
        "end_punctuations": {'"', "”", "»", "]", ")", ">"},
        "default_date_format": InterpretAsFormat.DATE_DMY,
        "replacements": [("’", "'")],  # normalize apostrophe
        **settings_args,
    }
    return TextProcessorSettings(lang="sw", **settings_args)


# -----------------------------------------------------------------------------
# Chinese (zh-cn, 汉语)
# -----------------------------------------------------------------------------


def get_zh_settings(lang_dir=None, **settings_args) -> TextProcessorSettings:
    """Create settings for Chinese"""

    # https://en.wikipedia.org/wiki/Chinese_punctuation
    settings_args = {
        "major_breaks": {"。", "！", "？"},
        "minor_breaks": {"；", "：", "，", "、", "……"},
        "begin_punctuations": {"（", "［", "【", "「", "﹁", '"', "《", "〈"},
        "end_punctuations": {"）", "］", " 】", "」", "﹂", '"', "》", "〉"},
        "word_breaks": {"‧"},
        "split_words": list,
        "join_str": "",
        **settings_args,
    }
    return TextProcessorSettings(lang="zh_CN", **settings_args)


# -----------------------------------------------------------------------------


class DelayedGraphemesToPhonemes:
    """Grapheme to phoneme guesser that loads on first use"""

    def __init__(
        self,
        model_path: typing.Union[str, Path],
        transform_func: typing.Optional[typing.Callable[[str], str]] = None,
        **g2p_args,
    ):
        self.model_path = model_path
        self.g2p: typing.Optional[GraphemesToPhonemes] = None
        self.transform_func = transform_func
        self.g2p_args = g2p_args

    def __call__(
        self, word: str, role: typing.Optional[str] = None
    ) -> typing.Optional[PHONEMES_TYPE]:
        if self.g2p is None:
            _LOGGER.debug(
                "Loading grapheme to phoneme CRF model from %s", self.model_path
            )
            self.g2p = GraphemesToPhonemes(self.model_path, **self.g2p_args)

        assert self.g2p is not None

        if self.transform_func is not None:
            word = self.transform_func(word)

        return self.g2p(word)


class DelayedPartOfSpeechTagger:
    """POS tagger that loads on first use"""

    def __init__(self, model_path: typing.Union[str, Path], **tagger_args):

        self.model_path = Path(model_path)
        self.tagger: typing.Optional[PartOfSpeechTagger] = None
        self.tagger_args = tagger_args

    def __call__(self, words: typing.Sequence[str]) -> typing.Sequence[str]:
        if self.tagger is None:
            _LOGGER.debug("Loading part of speech tagger from %s", self.model_path)
            self.tagger = PartOfSpeechTagger(self.model_path, **self.tagger_args)

        assert self.tagger is not None
        return self.tagger(words)


class DelayedSqlitePhonemizer:
    """Phonemizer that loads on first use"""

    def __init__(self, db_path: typing.Union[str, Path], **phonemizer_args):

        self.db_path = Path(db_path)
        self.phonemizer: typing.Optional[SqlitePhonemizer] = None
        self.phonemizer_args = phonemizer_args

    def __call__(
        self, word: str, role: typing.Optional[str] = None, do_transforms: bool = True
    ) -> typing.Optional[PHONEMES_TYPE]:
        if self.phonemizer is None:
            _LOGGER.debug("Connecting to lexicon database at %s", self.db_path)
            db_conn = sqlite3.connect(str(self.db_path))
            self.phonemizer = SqlitePhonemizer(db_conn=db_conn, **self.phonemizer_args)

        assert self.phonemizer is not None
        return self.phonemizer(word, role=role, do_transforms=do_transforms)
