# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

from __future__ import annotations

import json
import re
import time
from collections.abc import Callable
from typing import Any, Literal

import aqt.browser
from anki.cards import Card
from anki.collection import Config
from anki.tags import MARKED_TAG
from aqt import AnkiQt, gui_hooks, is_mac
from aqt.qt import (
    QCheckBox,
    QDialog,
    QDialogButtonBox,
    QKeySequence,
    QShortcut,
    Qt,
    QTimer,
    QVBoxLayout,
    qconnect,
)
from aqt.reviewer import replay_audio
from aqt.sound import av_player, play_clicked_audio
from aqt.theme import theme_manager
from aqt.utils import disable_help_button, restoreGeom, saveGeom, setWindowIcon, tr
from aqt.webview import AnkiWebView, AnkiWebViewKind

LastStateAndMod = tuple[str, int, int]


class Previewer(QDialog):
    _state: Literal["question", "answer"] = "question"
    _last_state: LastStateAndMod | None = None
    _card_changed = False
    _last_render: int | float = 0
    _timer: QTimer | None = None
    _show_both_sides = False

    def __init__(
        self,
        parent: aqt.browser.Browser | None,
        mw: AnkiQt,
        on_close: Callable[[], None],
    ) -> None:
        super().__init__(None, Qt.WindowType.Window)
        mw.garbage_collect_on_dialog_finish(self)
        self._open = True
        self._parent = parent
        self._close_callback = on_close
        self.mw = mw
        disable_help_button(self)
        setWindowIcon(self)
        gui_hooks.previewer_did_init(self)

    def card(self) -> Card | None:
        raise NotImplementedError

    def card_changed(self) -> bool:
        raise NotImplementedError

    def open(self) -> None:
        self._state = "question"
        self._last_state = None
        self._create_gui()
        self._setup_web_view()
        self.render_card()
        restoreGeom(self, "preview")
        self.show()

    def _create_gui(self) -> None:
        self.setWindowTitle(tr.actions_preview())

        self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
        qconnect(self.close_shortcut.activated, self.close)

        qconnect(self.finished, self._on_finished)
        self.silentlyClose = True
        self.vbox = QVBoxLayout()
        spacing = 6
        self.vbox.setContentsMargins(0, 0, 0, 0)
        self.vbox.setSpacing(spacing)
        self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER)
        self.vbox.addWidget(self._web)
        self.bbox = QDialogButtonBox()
        self.bbox.setContentsMargins(
            spacing, spacing if is_mac else 0, spacing, spacing
        )
        self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight)

        gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER)

        self._replay = self.bbox.addButton(
            tr.actions_replay_audio(), QDialogButtonBox.ButtonRole.ActionRole
        )
        assert self._replay is not None
        self._replay.setAutoDefault(False)
        self._replay.setShortcut(QKeySequence("R"))
        self._replay.setToolTip(tr.actions_shortcut_key(val="R"))
        qconnect(self._replay.clicked, self._on_replay_audio)

        both_sides_button = QCheckBox(tr.qt_misc_back_side_only())
        both_sides_button.setShortcut(QKeySequence("B"))
        both_sides_button.setToolTip(tr.actions_shortcut_key(val="B"))
        self.bbox.addButton(both_sides_button, QDialogButtonBox.ButtonRole.ActionRole)
        self._show_both_sides = self.mw.col.get_config_bool(
            Config.Bool.PREVIEW_BOTH_SIDES
        )
        both_sides_button.setChecked(self._show_both_sides)
        qconnect(both_sides_button.toggled, self._on_show_both_sides)

        self.vbox.addWidget(self.bbox)
        self.setLayout(self.vbox)

    def _on_finished(self, ok: int) -> None:
        saveGeom(self, "preview")
        self._on_close()

    def _on_replay_audio(self) -> None:
        assert self._web is not None
        card = self.card()
        assert card is not None

        gui_hooks.audio_will_replay(self._web, card, self._state == "question")

        if self._state == "question":
            replay_audio(card, True)
        elif self._state == "answer":
            replay_audio(card, False)

    def _on_close(self) -> None:
        self._open = False
        self._close_callback()

        assert self._web is not None

        self._web.cleanup()
        self._web = None

    def _setup_web_view(self) -> None:
        assert self._web is not None

        self._web.stdHtml(
            self.mw.reviewer.revHtml(),
            css=["css/reviewer.css"],
            js=[
                "js/mathjax.js",
                "js/vendor/mathjax/tex-chtml-full.js",
                "js/reviewer.js",
            ],
            context=self,
        )
        self._web.allow_drops = True
        self._web.eval("_blockDefaultDragDropBehavior();")
        self._web.set_bridge_command(self._on_bridge_cmd, self)

    def _on_bridge_cmd(self, cmd: str) -> Any:
        if cmd.startswith("play:"):
            card = self.card()
            assert card is not None

            play_clicked_audio(cmd, card)

    def _update_flag_and_mark_icons(self, card: Card | None) -> None:
        if card:
            flag = card.user_flag()
            marked = card.note(reload=True).has_tag(MARKED_TAG)
        else:
            flag = 0
            marked = False

        assert self._web is not None

        self._web.eval(f"_drawFlag({flag}); _drawMark({json.dumps(marked)});")

    def render_card(self) -> None:
        self.cancel_timer()
        # Keep track of whether render() has ever been called
        # with cardChanged=True since the last successful render
        self._card_changed |= self.card_changed()
        # avoid rendering in quick succession
        elap_ms = int((time.time() - self._last_render) * 1000)
        delay = 300
        if elap_ms < delay:
            self._timer = self.mw.progress.timer(
                delay - elap_ms, self._render_scheduled, False, parent=self
            )
        else:
            self._render_scheduled()

    def cancel_timer(self) -> None:
        if self._timer:
            self._timer.stop()
            self._timer = None

    def type_ans_preview_filter(
        self, txt: str, type: Literal["question", "answer"]
    ) -> str:
        return re.sub(r"\[\[type:[^]]+\]\]", "", txt)

    def _render_scheduled(self) -> None:
        self.cancel_timer()
        self._last_render = time.time()

        if not self._open:
            return
        c = self.card()
        self._update_flag_and_mark_icons(c)
        func = "_showQuestion"
        ans_txt = ""
        if not c:
            txt = tr.qt_misc_please_select_1_card()
            bodyclass = ""
            self._last_state = None
        else:
            if self._show_both_sides:
                self._state = "answer"
            elif self._card_changed:
                self._state = "question"

            currentState = self._state_and_mod()
            if currentState == self._last_state:
                # nothing has changed, avoid refreshing
                return

            # need to force reload even if answer
            txt = c.question(reload=True)
            ans_txt = c.answer()

            if self._state == "answer":
                func = "_showAnswer"
                txt = ans_txt
            txt = self.type_ans_preview_filter(txt, self._state)

            bodyclass = theme_manager.body_classes_for_card_ord(c.ord)

            assert self._web is not None

            if c.autoplay():
                self._web.setPlaybackRequiresGesture(False)
                if self._show_both_sides:
                    # if we're showing both sides at once, remove any audio
                    # from the answer that's appeared on the question already
                    question_audio = c.question_av_tags()
                    only_on_answer_audio = [
                        x for x in c.answer_av_tags() if x not in question_audio
                    ]
                    audio = question_audio + only_on_answer_audio
                elif self._state == "question":
                    audio = c.question_av_tags()
                else:
                    audio = c.answer_av_tags()
            else:
                audio = []
                self._web.setPlaybackRequiresGesture(True)
            gui_hooks.av_player_will_play_tags(audio, self._state, self)
            av_player.play_tags(audio)
            txt = self.mw.prepare_card_text_for_display(txt)
            txt = gui_hooks.card_will_show(txt, c, f"preview{self._state.capitalize()}")
            self._last_state = self._state_and_mod()

        js: str
        if self._state == "question":
            ans_txt = self.mw.col.media.escape_media_filenames(ans_txt)
            js = f"{func}({json.dumps(txt)}, {json.dumps(ans_txt)}, '{bodyclass}');"
        else:
            js = f"{func}({json.dumps(txt)}, '{bodyclass}');"

        assert self._web is not None
        self._web.eval(js)
        self._card_changed = False

    def _on_show_both_sides(self, toggle: bool) -> None:
        assert self._web is not None

        self._show_both_sides = toggle
        self.mw.col.set_config_bool(Config.Bool.PREVIEW_BOTH_SIDES, toggle)

        card = self.card()
        assert card is not None

        gui_hooks.previewer_will_redraw_after_show_both_sides_toggled(
            self._web, card, self._state == "question", toggle
        )

        if self._state == "answer" and not toggle:
            self._state = "question"
        self.render_card()

    def _state_and_mod(self) -> tuple[str, int, int]:
        c = self.card()

        assert c is not None

        n = c.note()
        n.load()
        return (self._state, c.id, n.mod)

    def state(self) -> str:
        return self._state


class MultiCardPreviewer(Previewer):
    def card(self) -> Card | None:
        # need to state explicitly it's not implement to avoid W0223
        raise NotImplementedError

    def card_changed(self) -> bool:
        # need to state explicitly it's not implement to avoid W0223
        raise NotImplementedError

    def _create_gui(self) -> None:
        super()._create_gui()
        self._prev = self.bbox.addButton(
            ">" if self.layoutDirection() == Qt.LayoutDirection.RightToLeft else "<",
            QDialogButtonBox.ButtonRole.ActionRole,
        )

        assert self._prev is not None

        self._prev.setAutoDefault(False)
        self._prev.setShortcut(QKeySequence("Left"))
        self._prev.setToolTip(tr.qt_misc_shortcut_key_left_arrow())

        self._next = self.bbox.addButton(
            "<" if self.layoutDirection() == Qt.LayoutDirection.RightToLeft else ">",
            QDialogButtonBox.ButtonRole.ActionRole,
        )

        assert self._next is not None

        self._next.setAutoDefault(True)
        self._next.setShortcut(QKeySequence("Right"))
        self._next.setToolTip(tr.qt_misc_shortcut_key_right_arrow_or_enter())

        qconnect(self._prev.clicked, self._on_prev)
        qconnect(self._next.clicked, self._on_next)

    def _on_prev(self) -> None:
        if self._state == "answer" and not self._show_both_sides:
            self._state = "question"
            self.render_card()
        else:
            self._on_prev_card()

    def _on_prev_card(self) -> None:
        pass

    def _on_next(self) -> None:
        if self._state == "question":
            self._state = "answer"
            self.render_card()
        else:
            self._on_next_card()

    def _on_next_card(self) -> None:
        pass

    def _updateButtons(self) -> None:
        if not self._open:
            return

        assert self._prev is not None
        assert self._next is not None

        self._prev.setEnabled(self._should_enable_prev())
        self._next.setEnabled(self._should_enable_next())

    def _should_enable_prev(self) -> bool:
        return self._state == "answer" and not self._show_both_sides

    def _should_enable_next(self) -> bool:
        return self._state == "question"

    def _on_close(self) -> None:
        super()._on_close()
        self._prev = None
        self._next = None


class BrowserPreviewer(MultiCardPreviewer):
    _last_card_id = 0
    _parent: aqt.browser.Browser | None

    def __init__(
        self, parent: aqt.browser.Browser, mw: AnkiQt, on_close: Callable[[], None]
    ) -> None:
        super().__init__(parent=parent, mw=mw, on_close=on_close)

    def card(self) -> Card | None:
        assert self._parent is not None

        if self._parent.singleCard:
            return self._parent.card
        else:
            return None

    def card_changed(self) -> bool:
        c = self.card()
        if not c:
            return True
        else:
            changed = c.id != self._last_card_id
            self._last_card_id = c.id
            return changed

    def _on_prev_card(self) -> None:
        assert self._parent is not None

        self._parent.onPreviousCard()

    def _on_next_card(self) -> None:
        assert self._parent is not None

        self._parent.onNextCard()

    def _should_enable_prev(self) -> bool:
        assert self._parent is not None

        return super()._should_enable_prev() or self._parent.has_previous_card()

    def _should_enable_next(self) -> bool:
        assert self._parent is not None

        return super()._should_enable_next() or self._parent.has_next_card()

    def _render_scheduled(self) -> None:
        super()._render_scheduled()
        self._updateButtons()
