# -*- coding: utf-8 -*-

# AwesomeTTS text-to-speech add-on for Anki
# Copyright (C) 2010-Present  Anki AwesomeTTS Development Team
#
# 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/>.

"""
Add-on package initialization
"""

from os.path import join
import os
import sys
from time import time

from PyQt5.QtCore import PYQT_VERSION_STR, Qt
from PyQt5.QtGui import QKeySequence

import anki
import aqt

from . import conversion as to, gui, paths, service
from .bundle import Bundle
from .config import Config
from .player import Player
from .router import Router
from .text import Sanitizer

__all__ = ['browser_menus', 'cards_button', 'config_menu', 'editor_button',
           'reviewer_hooks', 'sound_tag_delays', 
           'window_shortcuts']


def get_platform_info():
    """Exception-tolerant platform information for use with AGENT."""

    implementation = system_description = "???"
    python_version = "?.?.?"

    try:
        import platform
    except:  # catch-all, pylint:disable=bare-except
        pass
    else:
        try:
            implementation = platform.python_implementation()
        except:  # catch-all, pylint:disable=bare-except
            pass

        try:
            python_version = platform.python_version()
        except:  # catch-all, pylint:disable=bare-except
            pass

        try:
            system_description = platform.platform().replace('-', ' ')
        except:  # catch-all, pylint:disable=bare-except
            pass

    return "%s %s; %s" % (implementation, python_version, system_description)

VERSION = '1.18.0'

WEB = 'https://github.com/AwesomeTTS/awesometts-anki-addon'

AGENT = 'AwesomeTTS/%s (Anki %s; PyQt %s; %s)' % (VERSION, anki.version,
                                                  PYQT_VERSION_STR,
                                                  get_platform_info())


# Begin core class initialization and dependency setup, pylint:disable=C0103

if 'AWESOMETTS_DEBUG_LOGGING' in os.environ and os.environ['AWESOMETTS_DEBUG_LOGGING'] == 'enable':
    import logging 
    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger('awesometts')
    logger.setLevel(logging.DEBUG)
else:
    logger = Bundle(debug=lambda *a, **k: None, error=lambda *a, **k: None,
                    info=lambda *a, **k: None, warn=lambda *a, **k: None)

sequences = {key: QKeySequence()
             for key in ['browser_generator', 'browser_stripper',
                         'configurator', 'editor_generator', 'templater']}

config = Config(
    db=Bundle(path=paths.CONFIG,
              table='general',
              normalize=to.normalized_ascii),
    cols=[
        ('automaticAnswers', 'integer', True, to.lax_bool, int),
        ('automatic_answers_errors', 'integer', True, to.lax_bool, int),
        ('automaticQuestions', 'integer', True, to.lax_bool, int),
        ('automatic_questions_errors', 'integer', True, to.lax_bool, int),
        ('cache_days', 'integer', 365, int, int),
        ('delay_answers_onthefly', 'integer', 0, int, int),
        ('delay_answers_stored_ours', 'integer', 0, int, int),
        ('delay_answers_stored_theirs', 'integer', 0, int, int),
        ('delay_questions_onthefly', 'integer', 0, int, int),
        ('delay_questions_stored_ours', 'integer', 0, int, int),
        ('delay_questions_stored_theirs', 'integer', 0, int, int),
        ('ellip_note_newlines', 'integer', False, to.lax_bool, int),
        ('ellip_template_newlines', 'integer', False, to.lax_bool, int),
        ('extras', 'text', {}, to.deserialized_dict, to.compact_json),
        ('filenames', 'text', 'hash', str, str),
        ('filenames_human', 'text',
         '{{text}} ({{service}} {{voice}})', str, str),
        ('groups', 'text', {}, to.deserialized_dict, to.compact_json),
        ('lame_flags', 'text', '--quiet -q 2', str, str),
        ('last_mass_append', 'integer', True, to.lax_bool, int),
        ('last_mass_behavior', 'integer', True, to.lax_bool, int),
        ('last_mass_dest', 'text', 'Back', str, str),
        ('last_mass_source', 'text', 'Front', str, str),
        ('last_options', 'text', {}, to.deserialized_dict, to.compact_json),
        ('last_service', 'text', ('sapi5js' if 'win32' in sys.platform
                                  else 'say' if 'darwin' in sys.platform
                                  else 'yandex'), str, str),
        ('last_strip_mode', 'text', 'ours', str, str),
        ('launch_browser_generator', 'integer', Qt.ControlModifier | Qt.Key_T,
         to.nullable_key, to.nullable_int),
        ('launch_browser_stripper', 'integer', None, to.nullable_key,
         to.nullable_int),
        ('launch_configurator', 'integer', Qt.ControlModifier | Qt.Key_T,
         to.nullable_key, to.nullable_int),
        ('launch_editor_generator', 'integer', Qt.ControlModifier | Qt.Key_T,
         to.nullable_key, to.nullable_int),
        ('launch_templater', 'integer', Qt.ControlModifier | Qt.Key_T,
         to.nullable_key, to.nullable_int),
        ('otf_only_revealed_cloze', 'integer', False, to.lax_bool, int),
        ('otf_remove_hints', 'integer', False, to.lax_bool, int),
        ('presets', 'text', {}, to.deserialized_dict, to.compact_json),
        ('spec_note_count', 'text', '', str, str),
        ('spec_note_count_wrap', 'integer', True, to.lax_bool, int),
        ('spec_note_ellipsize', 'text', '', str, str),
        ('spec_note_strip', 'text', '', str, str),
        ('spec_template_count', 'text', '', str, str),
        ('spec_template_count_wrap', 'integer', True, to.lax_bool, int),
        ('spec_template_ellipsize', 'text', '', str, str),
        ('spec_template_strip', 'text', '', str, str),
        ('strip_note_braces', 'integer', False, to.lax_bool, int),
        ('strip_note_brackets', 'integer', False, to.lax_bool, int),
        ('strip_note_parens', 'integer', False, to.lax_bool, int),
        ('strip_template_braces', 'integer', False, to.lax_bool, int),
        ('strip_template_brackets', 'integer', False, to.lax_bool, int),
        ('strip_template_parens', 'integer', False, to.lax_bool, int),
        ('sub_note_cloze', 'text', 'anki', str, str),
        ('sub_template_cloze', 'text', 'anki', str, str),
        ('sul_note', 'text', [], to.substitution_list, to.substitution_json),
        ('sul_template', 'text', [], to.substitution_list,
         to.substitution_json),
        ('templater_cloze', 'integer', True, to.lax_bool, int),
        ('templater_field', 'text', 'Front', str, str),
        ('templater_hide', 'text', 'normal', str, str),
        ('templater_target', 'text', 'front', str, str),
        ('throttle_sleep', 'integer', 30, int, int),
        ('throttle_threshold', 'integer', 10, int, int),
        ('TTS_KEY_A', 'integer', Qt.Key_F4, to.nullable_key, to.nullable_int),
        ('TTS_KEY_Q', 'integer', Qt.Key_F3, to.nullable_key, to.nullable_int),
    ],
    logger=logger,
    events=[
    ],
)

try:
    from aqt.sound import av_player
    from anki.sound import SoundOrVideoTag

    def append_file(self, filename: str) -> None:
        self._enqueued.append(SoundOrVideoTag(filename=filename))
        self._play_next_if_idle()

    anki.sound.play = lambda filename: append_file(av_player, filename)
except ImportError:
    pass

player = Player(
    anki=Bundle(
        mw=aqt.mw,
        native=anki.sound.play,  # need direct reference, as this gets wrapped
        sound=anki.sound,  # for accessing queue member, which is not wrapped
    ),
    blank=paths.BLANK,
    config=config,
    logger=logger,
)

router = Router(
    services=Bundle(
        mappings=[
            ('abair', service.Abair),
            ('azure', service.Azure),
            ('baidu', service.Baidu),
            ('cambridge', service.Cambridge),
            ('collins', service.Collins),
            ('duden', service.Duden),
            ('ekho', service.Ekho),
            ('espeak', service.ESpeak),
            ('festival', service.Festival),
            ('fluencynl', service.FluencyNl),
            ('google', service.Google),
            ('googletts', service.GoogleTTS),
            ('howjsay', service.Howjsay),
            ('imtranslator', service.ImTranslator),
            ('ispeech', service.ISpeech),
            ('naver', service.Naver),
            ('neospeech', service.NeoSpeech),
            ('oddcast', service.Oddcast),
            ('oxford', service.Oxford),
            ('pico2wave', service.Pico2Wave),
            ('rhvoice', service.RHVoice),
            ('sapi5com', service.SAPI5COM),
            ('sapi5js', service.SAPI5JS),
            ('say', service.Say),
            ('spanishdict', service.SpanishDict),
            ('voicetext', service.VoiceText),
            ('wiktionary', service.Wiktionary),
            ('yandex', service.Yandex),
            ('youdao', service.Youdao),
            ('forvo', service.Forvo),
        ],
        dead=dict(
            ttsapicom="TTS-API.com has gone offline and can no longer be "
                      "used. Please switch to another service with English.",
        ),
        aliases=[('b', 'baidu'), ('g', 'google'), ('macosx', 'say'),
                 ('microsoft', 'sapi5js'), ('microsoftjs', 'sapi5js'),
                 ('microsoftjscript', 'sapi5js'), ('oed', 'oxford'),
                 ('osx', 'say'), ('sapi', 'sapi5js'), ('sapi5', 'sapi5js'),
                 ('sapi5jscript', 'sapi5js'), ('sapijs', 'sapi5js'),
                 ('sapijscript', 'sapi5js'), ('svox', 'pico2wave'),
                 ('svoxpico', 'pico2wave'), ('ttsapi', 'ttsapicom'),
                 ('windows', 'sapi5js'), ('windowsjs', 'sapi5js'),
                 ('windowsjscript', 'sapi5js'), ('y', 'yandex')],
        normalize=to.normalized_ascii,
        args=(),
        kwargs=dict(temp_dir=paths.TEMP,
                    lame_flags=lambda: config['lame_flags'],
                    normalize=to.normalized_ascii,
                    logger=logger,
                    ecosystem=Bundle(web=WEB, agent=AGENT)),
    ),
    cache_dir=paths.CACHE,
    temp_dir=join(paths.TEMP, '_awesometts_scratch_' + str(int(time()))),
    logger=logger,
    config=config,
)


STRIP_TEMPLATE_POSTHTML = [
    'whitespace',
    'sounds_univ',
    'filenames',
    ('within_parens', 'strip_template_parens'),
    ('within_brackets', 'strip_template_brackets'),
    ('within_braces', 'strip_template_braces'),
    ('char_remove', 'spec_template_strip'),
    ('counter', 'spec_template_count', 'spec_template_count_wrap'),
    ('char_ellipsize', 'spec_template_ellipsize'),
    ('custom_sub', 'sul_template'),
    'ellipses',
    'whitespace',
]

def bundlefail(message, text="Not available by addon.Bundle.downloader.fail"):
    aqt.utils.showCritical(message, aqt.mw)

addon = Bundle(
    config=config,
    downloader=Bundle(
        base=aqt.addons.GetAddons,
        superbase=aqt.addons.GetAddons.__bases__[0],
        args=[aqt.mw],
        kwargs=dict(),
        attrs=dict(
            form=Bundle(
                code=Bundle(text=lambda: '301952613'),
            ),
            mw=aqt.mw,
        ),
        fail=bundlefail,
    ),
    logger=logger,
    paths=Bundle(cache=paths.CACHE,
                 is_link=paths.ADDON_IS_LINKED),
    player=player,
    router=router,
    strip=Bundle(
        # n.b. cloze substitution logic happens first in both modes because:
        # - we need the <span>...</span> markup in on-the-fly to identify it
        # - Anki won't recognize cloze w/ HTML beginning/ending within braces
        # - the following 'html' rule will cleanse the HTML out anyway

        # for content directly from a note field (e.g. BrowserGenerator runs,
        # prepopulating a modal input based on some note field, where cloze
        # placeholders are still in their unprocessed state)
        from_note=Sanitizer([
            ('clozes_braced', 'sub_note_cloze'),
            ('newline_ellipsize', 'ellip_note_newlines'),
            'html',
            'whitespace',
            'sounds_univ',
            'filenames',
            ('within_parens', 'strip_note_parens'),
            ('within_brackets', 'strip_note_brackets'),
            ('within_braces', 'strip_note_braces'),
            ('char_remove', 'spec_note_strip'),
            ('counter', 'spec_note_count', 'spec_note_count_wrap'),
            ('char_ellipsize', 'spec_note_ellipsize'),
            ('custom_sub', 'sul_note'),
            'ellipses',
            'whitespace',
        ], config=config, logger=logger),

        # for cleaning up already-processed HTML templates (e.g. on-the-fly,
        # where cloze is marked with <span class=cloze></span> tags)
        from_template_front=Sanitizer([
            ('clozes_rendered', 'sub_template_cloze'),
            'hint_links',
            ('hint_content', 'otf_remove_hints'),
            ('newline_ellipsize', 'ellip_template_newlines'),
            'html',
        ] + STRIP_TEMPLATE_POSTHTML, config=config, logger=logger),

        # like the previous, but for the back sides of cards
        from_template_back=Sanitizer([
            ('clozes_revealed', 'otf_only_revealed_cloze'),
            'hint_links',
            ('hint_content', 'otf_remove_hints'),
            ('newline_ellipsize', 'ellip_template_newlines'),
            'html',
        ] + STRIP_TEMPLATE_POSTHTML, config=config, logger=logger),

        # for cleaning up text from unknown sources (e.g. system clipboard);
        # n.b. clozes_revealed is not used here without the card context and
        # it would be a weird thing to apply to the clipboard content anyway
        from_unknown=Sanitizer([
            ('clozes_braced', 'sub_note_cloze'),
            ('clozes_rendered', 'sub_template_cloze'),
            'hint_links',
            ('hint_content', 'otf_remove_hints'),
            ('newline_ellipsize', 'ellip_note_newlines'),
            ('newline_ellipsize', 'ellip_template_newlines'),
            'html',
            'html',  # clipboards often have escaped HTML, so we run twice
            'whitespace',
            'sounds_univ',
            'filenames',
            ('within_parens', ['strip_note_parens', 'strip_template_parens']),
            ('within_brackets', ['strip_note_brackets',
                                 'strip_template_brackets']),
            ('within_braces', ['strip_note_braces', 'strip_template_braces']),
            ('char_remove', 'spec_note_strip'),
            ('char_remove', 'spec_template_strip'),
            ('counter', 'spec_note_count', 'spec_note_count_wrap'),
            ('counter', 'spec_template_count', 'spec_template_count_wrap'),
            ('char_ellipsize', 'spec_note_ellipsize'),
            ('char_ellipsize', 'spec_template_ellipsize'),
            ('custom_sub', 'sul_note'),
            ('custom_sub', 'sul_template'),
            'ellipses',
            'whitespace',
        ], config=config, logger=logger),

        # for direct user input (e.g. previews, EditorGenerator insertion)
        from_user=Sanitizer(rules=['ellipses', 'whitespace'], logger=logger),

        # target sounds specifically
        sounds=Bundle(
            # using Anki's method (used if we need to reproduce how Anki does
            # something, e.g. when Reviewer emulates {{FrontSide}})
            anki=anki.sound.stripSounds,

            # using AwesomeTTS's methods (which have access to precompiled re
            # objects, usable for everything else, e.g. when BrowserGenerator
            # or BrowserStripper need to remove old sounds)
            ours=Sanitizer(rules=['sounds_ours', 'filenames'], logger=logger),
            theirs=Sanitizer(rules=['sounds_theirs'], logger=logger),
            univ=Sanitizer(rules=['sounds_univ', 'filenames'], logger=logger),
        ),
    ),
    version=VERSION,
    web=WEB,
)

# End core class initialization and dependency setup, pylint:enable=C0103


# GUI interaction with Anki
# n.b. be careful wrapping methods that have return values (see anki.hooks);
#      in general, only the 'before' mode absolves us of responsibility

# These are all called manually from the __init__.py loader so that if there
# is some sort of breakage with a specific component, it could be possibly
# disabled easily by users who are not utilizing that functionality.


def browser_menus():
    """
    Gives user access to mass generator, MP3 stripper, and the hook that
    disables and enables it upon selection of items.
    """

    from PyQt5 import QtWidgets

    def on_setup_menus(browser):
        """Create an AwesomeTTS menu and add browser actions to it."""

        menu = QtWidgets.QMenu("Awesome&TTS", browser.form.menubar)
        browser.form.menubar.addMenu(menu)

        gui.Action(
            target=Bundle(
                constructor=gui.BrowserGenerator,
                args=(),
                kwargs=dict(browser=browser,
                            addon=addon,
                            alerts=aqt.utils.showWarning,
                            ask=aqt.utils.getText,
                            parent=browser),
            ),
            text="&Add Audio to Selected...",
            sequence=sequences['browser_generator'],
            parent=menu,
        )
        gui.Action(
            target=Bundle(
                constructor=gui.BrowserStripper,
                args=(),
                kwargs=dict(browser=browser,
                            addon=addon,
                            alerts=aqt.utils.showWarning,
                            parent=browser),
            ),
            text="&Remove Audio from Selected...",
            sequence=sequences['browser_stripper'],
            parent=menu,
        )

    def update_title_wrapper(browser):
        """Enable/disable AwesomeTTS menu items upon selection."""

        enabled = bool(browser.form.tableView.selectionModel().selectedRows())
        for action in browser.findChildren(gui.Action):
            action.setEnabled(enabled)

    anki.hooks.addHook(
        'browser.setupMenus',
        on_setup_menus,
    )

    aqt.browser.Browser.updateTitle = anki.hooks.wrap(
        aqt.browser.Browser.updateTitle,
        update_title_wrapper,
        'before',
    )


def cache_control():
    """Registers a hook to handle cache control on session exits."""

    def on_unload_profile():
        """
        Finds MP3s in the cache directory older than the user's
        configured cache limit and attempts to remove them.
        """

        from os import listdir, unlink

        cache = paths.CACHE

        try:
            filenames = listdir(cache)
        except:  # allow silent failure, pylint:disable=bare-except
            return
        if not filenames:
            return

        prospects = (join(cache, filename) for filename in filenames)

        if config['cache_days']:
            from os.path import getmtime

            limit = time() - 86400 * config['cache_days']
            targets = (prospect for prospect in prospects
                       if getmtime(prospect) < limit)
        else:
            targets = prospects

        for target in targets:
            try:
                unlink(target)
            except:  # skip broken files, pylint:disable=bare-except
                pass

    anki.hooks.addHook('unloadProfile', on_unload_profile)


def cards_button():
    """Provides access to the templater helper."""

    from aqt import clayout

    clayout.CardLayout.setupButtons = anki.hooks.wrap(
        clayout.CardLayout.setupButtons,
        lambda card_layout: card_layout.buttons.insertWidget(
            # Now, the card layout for regular notes has 7 buttons/stretchers
            # and the one for cloze notes has 6 (as it lacks a "Flip" button);
            # position 3 puts our button after "Add Field", but in the event
            # that the form suddenly has a different number of buttons, let's
            # just fallback to the far left position

            3 if card_layout.buttons.count() in [6, 7] else 0,
            gui.Button(
                text="Add &TTS",
                tooltip="Insert a tag for on-the-fly playback w/ AwesomeTTS",
                sequence=sequences['templater'],
                target=Bundle(
                    constructor=gui.Templater,
                    args=(),
                    kwargs=dict(card_layout=card_layout,
                                addon=addon,
                                alerts=aqt.utils.showWarning,
                                ask=aqt.utils.getText,
                                parent=card_layout),
                ),
            ),
        ),
        'after',  # must use 'after' so that 'buttons' attribute is set
    )


def config_menu():
    """
    Adds a menu item to the Tools menu in Anki's main window for
    launching the configuration dialog.
    """

    gui.Action(
        target=Bundle(
            constructor=gui.Configurator,
            args=(),
            kwargs=dict(addon=addon, sul_compiler=to.substitution_compiled,
                        alerts=aqt.utils.showWarning,
                        ask=aqt.utils.getText,
                        parent=aqt.mw),
        ),
        text="Awesome&TTS...",
        sequence=sequences['configurator'],
        parent=aqt.mw.form.menuTools,
    )


def editor_button():
    """
    Enable the generation of a single audio clip through the editor,
    which is present in the "Add" and browser windows.
    """

    anki.hooks.addHook(
        'setupEditorButtons',
        lambda buttons, editor: gui.HTMLButton(
            buttons, editor,
            link_id='awesometts_btn',
            tooltip="Record and insert an audio clip here w/ AwesomeTTS",
            sequence=sequences['editor_generator'],
            target=Bundle(
                constructor=gui.EditorGenerator,
                args=(),
                kwargs=dict(editor=editor,
                            addon=addon,
                            alerts=aqt.utils.showWarning,
                            ask=aqt.utils.getText,
                            parent=editor.parentWindow),
            )
        ).buttons
    )

    anki.hooks.addHook(
        'setupEditorShortcuts',
        lambda shortcuts, editor: shortcuts.append(
            (
                sequences['editor_generator'].toString(),
                editor._links['awesometts_btn']
            )
        )
    )

    # TODO: Editor buttons are now in the WebView, not sure how (and if)
    # we should implement muzzling. Please see:
    # https://github.com/dae/anki/commit/a001553f66efe75e660eb0702cd29a9d62503fc4
    """
    aqt.editor.Editor.enableButtons = anki.hooks.wrap(
        aqt.editor.Editor.enableButtons,
        lambda editor, val=True: (
            editor.widget.findChild(gui.Button).setEnabled(val),

            # Temporarily disable any AwesomeTTS menu shortcuts in the Browser
            # window so that if a shortcut combination has been re-used
            # between the editor button and those, the "local" shortcut works.
            # Has no effect on "Add" window (the child list will be empty).
            [action.muzzle(val) for action
             in editor.parentWindow.findChildren(gui.Action)],
        ),
        'before',
    )
    """


def reviewer_hooks():
    """
    Enables support for AwesomeTTS to automatically play text-to-speech
    tags and to also do playback on-demand via shortcut keys and the
    context menu.
    """

    from PyQt5.QtCore import QEvent
    from PyQt5.QtWidgets import QMenu

    reviewer = gui.Reviewer(addon=addon,
                            alerts=aqt.utils.showWarning,
                            mw=aqt.mw)

    # automatic playback

    anki.hooks.addHook(
        'showQuestion',
        lambda: reviewer.card_handler('question', aqt.mw.reviewer.card),
    )

    anki.hooks.addHook(
        'showAnswer',
        lambda: reviewer.card_handler('answer', aqt.mw.reviewer.card),
    )

    # shortcut-triggered playback

    reviewer_filter = gui.Filter(
        relay=lambda event: reviewer.key_handler(
            key_event=event,
            state=aqt.mw.reviewer.state,
            card=aqt.mw.reviewer.card,
            replay_audio=aqt.mw.reviewer.replayAudio,
        ),

        when=lambda event: (aqt.mw.state == 'review' and
                            event.type() == QEvent.KeyPress and
                            not event.isAutoRepeat() and
                            not event.spontaneous()),

        parent=aqt.mw,  # prevents filter from being garbage collected
    )

    aqt.mw.installEventFilter(reviewer_filter)

    # context menu playback

    strip = Sanitizer([('newline_ellipsize', 'ellip_template_newlines')] +
                      STRIP_TEMPLATE_POSTHTML, config=config, logger=logger)

    def on_context_menu(web_view, menu):
        """Populate context menu, given the context/configuration."""

        window = web_view.window()

        try:  # this works for web views embedded in editor windows
            atts_button = web_view.editor.widget.findChild(gui.Button)
        except AttributeError:
            atts_button = None

        say_text = config['presets'] and strip(web_view.selectedText())

        tts_card = tts_side = None
        tts_shortcuts = False
        try:  # this works for web views in the reviewer and template dialog
            if window is aqt.mw and aqt.mw.state == 'review':
                tts_card = aqt.mw.reviewer.card
                tts_side = aqt.mw.reviewer.state
                tts_shortcuts = True
            elif web_view.objectName() == 'mainText':  # card template dialog
                parent_name = web_view.parentWidget().objectName()
                tts_card = window.card
                tts_side = ('question' if parent_name == 'groupBox'
                            else 'answer' if parent_name == 'groupBox_2'
                            else None)
        except Exception:  # just in case, pylint:disable=broad-except
            pass

        tts_question = tts_card and tts_side and \
            reviewer.has_tts('question', tts_card)
        tts_answer = tts_card and tts_side == 'answer' and \
            reviewer.has_tts('answer', tts_card)

        if not (atts_button or say_text or tts_question or tts_answer):
            return

        submenu = QMenu("Awesome&TTS", menu)
        submenu.setIcon(gui.ICON)

        needs_separator = False

        if atts_button:
            submenu.addAction(
                "Add MP3 to the Note",
                lambda: atts_button.click() if atts_button.isEnabled()
                else aqt.utils.showWarning(
                    "Select the note field to which you want to add an MP3.",
                    window,
                )
            )
            needs_separator = True

        if say_text:
            say_display = (say_text if len(say_text) < 25
                           else say_text[0:20].rstrip(' .') + "...")

            if config['presets']:
                if needs_separator:
                    submenu.addSeparator()
                else:
                    needs_separator = True

                def preset_glue(xxx_todo_changeme):
                    """Closure for callback handler to access `preset`."""
                    (name, preset) = xxx_todo_changeme
                    submenu.addAction(
                        'Say "%s" w/ %s' % (say_display, name),
                        lambda: reviewer.selection_handler(say_text,
                                                           preset,
                                                           window),
                    )
                for item in sorted(config['presets'].items(),
                                   key=lambda item: item[0].lower()):
                    preset_glue(item)

            if config['groups']:
                if needs_separator:
                    submenu.addSeparator()
                else:
                    needs_separator = True

                def group_glue(xxx_todo_changeme1):
                    """Closure for callback handler to access `group`."""
                    (name, group) = xxx_todo_changeme1
                    submenu.addAction(
                        'Say "%s" w/ %s' % (say_display, name),
                        lambda: reviewer.selection_handler_group(say_text,
                                                                 group,
                                                                 window),
                    )
                for item in sorted(config['groups'].items(),
                                   key=lambda item: item[0].lower()):
                    group_glue(item)

        if tts_question or tts_answer:
            if needs_separator:
                submenu.addSeparator()

            if tts_question:
                submenu.addAction(
                    "Play On-the-Fly TTS from Question Side",
                    lambda: reviewer.nonselection_handler('question', tts_card,
                                                          window),
                    tts_shortcuts and config['tts_key_q'] or 0,
                )

            if tts_answer:
                submenu.addAction(
                    "Play On-the-Fly TTS from Answer Side",
                    lambda: reviewer.nonselection_handler('answer', tts_card,
                                                          window),
                    tts_shortcuts and config['tts_key_a'] or 0,
                )

        menu.addMenu(submenu)

    anki.hooks.addHook('AnkiWebView.contextMenuEvent', on_context_menu)
    anki.hooks.addHook('EditorWebView.contextMenuEvent', on_context_menu)
    anki.hooks.addHook('Reviewer.contextMenuEvent',
                       lambda reviewer, menu:
                       on_context_menu(reviewer.web, menu))


def sound_tag_delays():
    """
    Enables support for the following sound delay configuration options:

    - delay_questions_stored_ours (AwesomeTTS MP3s on questions)
    - delay_questions_stored_theirs (non-AwesomeTTS MP3s on questions)
    - delay_answers_stored_ours (AwesomeTTS MP3s on answers)
    - delay_answers_stored_theirs (non-AwesomeTTS MP3s on answers)
    """

    anki.sound.play = player.native_wrapper


def temp_files():
    """Remove temporary files upon session exit."""

    def on_unload_profile():
        """
        Finds scratch directories in the temporary path, removes their
        files, then removes the directories themselves.
        """

        from os import listdir, unlink, rmdir
        from os.path import isdir

        temp = paths.TEMP

        try:
            subdirs = [join(temp, filename) for filename in listdir(temp)
                       if filename.startswith('_awesometts_scratch')]
        except:  # allow silent failure, pylint:disable=bare-except
            return
        if not subdirs:
            return

        for subdir in subdirs:
            if isdir(subdir):
                for filename in listdir(subdir):
                    try:
                        unlink(join(subdir, filename))
                    except:  # skip busy files, pylint:disable=bare-except
                        pass
                try:
                    rmdir(subdir)
                except:  # allow silent failure, pylint:disable=bare-except
                    pass

    anki.hooks.addHook('unloadProfile', on_unload_profile)


def window_shortcuts():
    """Enables shortcuts to launch windows."""

    def on_sequence_change(new_config):
        """Update sequences on configuration changes."""
        for key, sequence in sequences.items():
            new_sequence = QKeySequence(new_config['launch_' + key] or None)
            sequence.swap(new_sequence)

        try:
            aqt.mw.form.menuTools.findChild(gui.Action). \
                setShortcut(sequences['configurator'])
        except AttributeError:  # we do not have a config menu
            pass

    on_sequence_change(config)  # set config menu if created before we ran
    config.bind(['launch_' + key for key in sequences.keys()],
                on_sequence_change)