#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

import os
import re

from fenrirscreenreader.core import debug
from fenrirscreenreader.core.i18n import _


class DiffReviewManager:
    def __init__(self):
        self.env = None
        self.active = False
        self.loaded_path = ""
        self.lines = []
        self.line_meta = []
        self.file_sections = []
        self.hunk_sections = []
        self.file_starts = []
        self.hunk_starts = []
        self.added_lines = []
        self.removed_lines = []
        self.current_line = -1
        self.bindings_backup = None
        self.raw_bindings_backup = None
        self.total_added = 0
        self.total_removed = 0
        self.current_char = 0

    def initialize(self, environment):
        self.env = environment

    def shutdown(self):
        if self.active:
            self._restore_bindings()
        self.active = False

    def is_active(self):
        return self.active

    def has_loaded_diff(self):
        return len(self.lines) > 0

    def get_loaded_path(self):
        return self.loaded_path

    def toggle_mode(self):
        if self.active:
            self.disable_mode()
            self.present_mode_message(
                _("Diff review mode disabled."),
            )
            return False

        clipboard_path = self._get_clipboard_path_or_none()
        if (
            clipboard_path
            and (
                (not self.has_loaded_diff())
                or (self.loaded_path != clipboard_path)
            )
        ):
            if not self.load_file(clipboard_path):
                return False
        elif not self.has_loaded_diff():
            self.present_error(
                _("Clipboard does not contain full path to diff file.")
            )
            return False

        enabled = self.enable_mode()
        if enabled:
            self.present_mode_message(
                _("Diff review mode enabled."),
            )
        return enabled

    def enable_mode(self):
        if self.active:
            return True
        if not self.has_loaded_diff():
            return False
        self._install_diff_bindings()
        self.active = True
        return True

    def disable_mode(self):
        if not self.active:
            return
        self._restore_bindings()
        self.active = False

    def load_from_clipboard(self):
        clipboard_path = self._get_clipboard_path_or_none()
        if not clipboard_path:
            self.present_error(
                _("Clipboard does not contain full path to diff file.")
            )
            return False

        return self.load_file(clipboard_path)

    def reload_file(self):
        if not self.loaded_path:
            self.present_error(_("No diff file loaded."))
            return False
        return self.load_file(self.loaded_path)

    def load_file(self, file_path):
        normalized_path = os.path.abspath(os.path.expanduser(file_path))
        if not os.path.exists(normalized_path):
            self.present_error(
                _("Diff file not found: ") + normalized_path
            )
            return False
        if os.path.isdir(normalized_path):
            self.present_error(
                _("Diff path is a directory: ") + normalized_path
            )
            return False
        if not os.access(normalized_path, os.R_OK):
            self.present_error(
                _("Cannot read diff file: ") + normalized_path
            )
            return False

        try:
            with open(normalized_path, "r", encoding="utf-8", errors="replace") as diff_file:
                file_lines = diff_file.read().splitlines()
        except Exception as load_error:
            self._write_debug(
                "DiffReviewManager load_file: " + str(load_error),
                debug.DebugLevel.ERROR,
            )
            self.present_error(
                _("Failed to load diff file: ") + normalized_path
            )
            return False

        self.present_mode_message(
            _("Loading ") + normalized_path + ".",
        )
        self._parse_lines(file_lines, normalized_path)
        self.present_mode_message(
            _("Diff loaded. ")
            + str(len(self.file_sections))
            + _(" files changed. ")
            + str(self.total_added)
            + _(" additions. ")
            + str(self.total_removed)
            + _(" removals."),
        )
        return True

    def _parse_lines(self, file_lines, loaded_path):
        self.lines = file_lines
        self.loaded_path = loaded_path
        self.line_meta = []
        self.file_sections = []
        self.hunk_sections = []
        self.file_starts = []
        self.hunk_starts = []
        self.added_lines = []
        self.removed_lines = []
        self.current_line = -1
        self.total_added = 0
        self.total_removed = 0
        self.current_char = 0

        has_git_header = any(
            line.startswith("diff --git ") for line in self.lines
        )

        current_file_index = -1
        current_hunk_index = -1
        for line_index, line in enumerate(self.lines):
            role = self._classify_line(line)

            if self._starts_new_file_section(
                line=line,
                has_git_header=has_git_header,
            ):
                current_file_index = self._add_file_section(line_index)
                current_hunk_index = -1

            if (
                current_file_index == -1
                and role
                in {
                    "hunk_header",
                    "added_line",
                    "removed_line",
                    "context_line",
                    "note_line",
                }
            ):
                current_file_index = self._add_file_section(line_index)

            if role == "hunk_header":
                if current_file_index == -1:
                    current_file_index = self._add_file_section(line_index)
                current_hunk_index = self._add_hunk_section(
                    line_index=line_index,
                    file_index=current_file_index,
                )

            if role in {"added_line", "removed_line"} and current_hunk_index == -1:
                if current_file_index == -1:
                    current_file_index = self._add_file_section(line_index)
                current_hunk_index = self._add_hunk_section(
                    line_index=line_index,
                    file_index=current_file_index,
                )

            if role == "added_line":
                self.added_lines.append(line_index)
                self.total_added += 1
                if current_file_index != -1:
                    self.file_sections[current_file_index]["added_count"] += 1
                if current_hunk_index != -1:
                    self.hunk_sections[current_hunk_index]["added_count"] += 1
            elif role == "removed_line":
                self.removed_lines.append(line_index)
                self.total_removed += 1
                if current_file_index != -1:
                    self.file_sections[current_file_index]["removed_count"] += 1
                if current_hunk_index != -1:
                    self.hunk_sections[current_hunk_index]["removed_count"] += 1

            self.line_meta.append(
                {
                    "role": role,
                    "file_index": current_file_index,
                    "hunk_index": current_hunk_index,
                }
            )

        for file_index, file_section in enumerate(self.file_sections):
            file_hunks = file_section["hunk_indices"]
            for local_position, hunk_index in enumerate(file_hunks, start=1):
                self.hunk_sections[hunk_index]["position_in_file"] = local_position
                self.hunk_sections[hunk_index]["hunks_in_file"] = len(file_hunks)

    def _classify_line(self, line):
        if line.startswith("\\ No newline at end of file"):
            return "note_line"
        if self._is_file_header(line):
            return "file_header"
        if line.startswith("@@"):
            return "hunk_header"
        if line.startswith("+") and not line.startswith("+++ "):
            return "added_line"
        if line.startswith("-") and not line.startswith("--- "):
            return "removed_line"
        return "context_line"

    def _get_clipboard_path_or_none(self):
        if self.env["runtime"]["MemoryManager"].is_index_list_empty(
            "clipboardHistory"
        ):
            return None

        clipboard_value = self.env["runtime"]["MemoryManager"].get_index_list_element(
            "clipboardHistory"
        )
        if not isinstance(clipboard_value, str):
            return None

        clipboard_path = clipboard_value.strip().strip("\"'")
        clipboard_path = os.path.expanduser(clipboard_path)
        if not os.path.isabs(clipboard_path):
            return None
        return os.path.abspath(clipboard_path)

    def _is_file_header(self, line):
        file_header_prefixes = (
            "diff --git ",
            "--- ",
            "+++ ",
            "index ",
            "old mode ",
            "new mode ",
            "deleted file mode ",
            "new file mode ",
            "rename from ",
            "rename to ",
            "similarity index ",
            "dissimilarity index ",
            "Binary files ",
        )
        return line.startswith(file_header_prefixes)

    def _starts_new_file_section(self, line, has_git_header):
        if has_git_header:
            return line.startswith("diff --git ")
        return line.startswith("--- ")

    def _add_file_section(self, line_index):
        file_section = {
            "start_line": line_index,
            "added_count": 0,
            "removed_count": 0,
            "hunk_indices": [],
        }
        self.file_sections.append(file_section)
        self.file_starts.append(line_index)
        return len(self.file_sections) - 1

    def _add_hunk_section(self, line_index, file_index):
        hunk_section = {
            "start_line": line_index,
            "file_index": file_index,
            "added_count": 0,
            "removed_count": 0,
            "position_in_file": 0,
            "hunks_in_file": 0,
        }
        self.hunk_sections.append(hunk_section)
        self.hunk_starts.append(line_index)
        hunk_index = len(self.hunk_sections) - 1
        if file_index != -1:
            self.file_sections[file_index]["hunk_indices"].append(hunk_index)
        return hunk_index

    def next_file_section(self):
        self._jump_to_anchor(
            anchors=self.file_starts,
            previous=False,
            empty_message=_("No file sections in loaded diff."),
            edge_message=_("No further file sections."),
            announce_role="file",
        )

    def prev_file_section(self):
        self._jump_to_anchor(
            anchors=self.file_starts,
            previous=True,
            empty_message=_("No file sections in loaded diff."),
            edge_message=_("No previous file sections."),
            announce_role="file",
        )

    def next_hunk(self):
        self._jump_to_anchor(
            anchors=self.hunk_starts,
            previous=False,
            empty_message=_("No hunks in loaded diff."),
            edge_message=_("No further hunks."),
            announce_role="hunk",
        )

    def prev_hunk(self):
        self._jump_to_anchor(
            anchors=self.hunk_starts,
            previous=True,
            empty_message=_("No hunks in loaded diff."),
            edge_message=_("No previous hunks."),
            announce_role="hunk",
        )

    def next_added(self):
        self._jump_to_anchor(
            anchors=self.added_lines,
            previous=False,
            empty_message=_("No added lines in loaded diff."),
            edge_message=_("No further added lines."),
            announce_role="added",
        )

    def prev_added(self):
        self._jump_to_anchor(
            anchors=self.added_lines,
            previous=True,
            empty_message=_("No added lines in loaded diff."),
            edge_message=_("No previous added lines."),
            announce_role="added",
        )

    def next_removed(self):
        self._jump_to_anchor(
            anchors=self.removed_lines,
            previous=False,
            empty_message=_("No removed lines in loaded diff."),
            edge_message=_("No further removed lines."),
            announce_role="removed",
        )

    def prev_removed(self):
        self._jump_to_anchor(
            anchors=self.removed_lines,
            previous=True,
            empty_message=_("No removed lines in loaded diff."),
            edge_message=_("No previous removed lines."),
            announce_role="removed",
        )

    def present_summary(self):
        if not self.has_loaded_diff():
            self.present_mode_message(_("No diff file loaded."))
            return

        summary = (
            _("Diff summary. Files changed: ")
            + str(len(self.file_sections))
            + _(". Additions: ")
            + str(self.total_added)
            + _(". Removals: ")
            + str(self.total_removed)
            + "."
        )

        if self.current_line >= 0 and self.current_line < len(self.line_meta):
            location = self._build_location_text(self.current_line)
            if location:
                summary += " " + location

        self.present_mode_message(summary)

    def present_key_help(self):
        help_text = _(
            "Diff keys: h next hunk, shift h previous hunk, a next addition, "
            "shift a previous addition, d next removal, shift d previous removal, "
            "f next file section, shift f previous file section, up previous line, "
            "down next line, left previous character, right next character, "
            "control left previous word, control right next word, s summary, "
            "escape exit diff mode."
        )
        self.present_mode_message(help_text)

    def next_line(self):
        if not self.has_loaded_diff():
            self.present_mode_message(_("No diff file loaded."))
            return
        if self.current_line < 0:
            self.current_line = 0
            self.current_char = 0
            self._announce_current_line()
            return
        if self.current_line >= len(self.lines) - 1:
            self.present_mode_message(_("End of diff."))
            return
        self.current_line += 1
        self.current_char = 0
        self._announce_current_line()

    def prev_line(self):
        if not self.has_loaded_diff():
            self.present_mode_message(_("No diff file loaded."))
            return
        if self.current_line < 0:
            self.current_line = 0
            self.current_char = 0
            self._announce_current_line()
            return
        if self.current_line <= 0:
            self.present_mode_message(_("Start of diff."))
            return
        self.current_line -= 1
        self.current_char = 0
        self._announce_current_line()

    def next_char(self):
        if not self._ensure_current_line():
            return
        line_text = self.lines[self.current_line]
        if len(line_text) == 0:
            self.present_mode_message(_("blank"))
            return
        if self.current_char >= len(line_text) - 1:
            self.present_mode_message(_("End of line."))
            return
        self.current_char += 1
        self._announce_current_char()

    def prev_char(self):
        if not self._ensure_current_line():
            return
        line_text = self.lines[self.current_line]
        if len(line_text) == 0:
            self.present_mode_message(_("blank"))
            return
        if self.current_char <= 0:
            self.present_mode_message(_("Start of line."))
            return
        self.current_char -= 1
        self._announce_current_char()

    def next_word(self):
        if not self._ensure_current_line():
            return
        line_text = self.lines[self.current_line]
        word_spans = self._get_word_spans(line_text)
        if len(word_spans) == 0:
            self.present_mode_message(_("No words on this line."))
            return

        next_word_start = None
        for start, end in word_spans:
            if start > self.current_char:
                next_word_start = start
                break
            if start <= self.current_char < end:
                continue

        if next_word_start is None:
            self.present_mode_message(_("No next word on this line."))
            return

        self.current_char = next_word_start
        self._announce_word_at_current_char()

    def prev_word(self):
        if not self._ensure_current_line():
            return
        line_text = self.lines[self.current_line]
        word_spans = self._get_word_spans(line_text)
        if len(word_spans) == 0:
            self.present_mode_message(_("No words on this line."))
            return

        prev_word_start = None
        for start, end in word_spans:
            if end - 1 < self.current_char:
                prev_word_start = start
            elif start <= self.current_char < end:
                break
            else:
                break

        if prev_word_start is None:
            self.present_mode_message(_("No previous word on this line."))
            return

        self.current_char = prev_word_start
        self._announce_word_at_current_char()

    def _jump_to_anchor(
        self,
        anchors,
        previous,
        empty_message,
        edge_message,
        announce_role,
    ):
        if not self.has_loaded_diff():
            self.present_mode_message(_("No diff file loaded."))
            return

        if len(anchors) == 0:
            self.present_mode_message(empty_message)
            return

        new_line = self._find_next_anchor(anchors, previous)
        if new_line is None:
            self.present_mode_message(edge_message)
            return

        self.current_line = new_line
        self.current_char = 0
        self._announce_line(announce_role, new_line)

    def _find_next_anchor(self, anchors, previous):
        if self.current_line < 0:
            if previous:
                return anchors[-1]
            return anchors[0]

        if previous:
            for anchor_line in reversed(anchors):
                if anchor_line < self.current_line:
                    return anchor_line
            return None

        for anchor_line in anchors:
            if anchor_line > self.current_line:
                return anchor_line
        return None

    def _announce_line(self, announce_role, line_index):
        line_text = self.lines[line_index] if line_index < len(self.lines) else ""
        line_meta = self.line_meta[line_index]
        role = line_meta["role"]
        verbosity = self._get_diff_verbosity()

        if announce_role == "file":
            self._announce_file_section(line_index, line_meta)
            return
        if announce_role == "hunk":
            self._announce_hunk_section(line_index, line_meta, line_text)
            return

        if role == "added_line":
            base_message = _("Added line.")
        elif role == "removed_line":
            base_message = _("Removed line.")
        else:
            base_message = _("Diff line.")

        message = base_message + " " + self._build_location_text(line_index)
        if verbosity == "verbose":
            content = self._line_content_for_speech(line_text, role)
            message = message + " " + content
        self.present_mode_message(message)

    def _announce_current_line(self):
        if self.current_line < 0 or self.current_line >= len(self.lines):
            self.present_mode_message(_("No diff file loaded."))
            return
        line_text = self.lines[self.current_line]
        role = self.line_meta[self.current_line]["role"]

        if role == "context_line":
            formatted_classic_diff = self._format_classic_diff_command(line_text)
            if formatted_classic_diff:
                self.present_mode_message(formatted_classic_diff)
                return

        role_prefix = {
            "added_line": _("Added"),
            "removed_line": _("Removed"),
            "hunk_header": _("Hunk header"),
            "file_header": _("File header"),
            "note_line": _("Note"),
            "context_line": "",
        }.get(role, _("Line"))
        if line_text.strip() == "":
            if role_prefix:
                self.present_mode_message(role_prefix + ": " + _("blank"))
            else:
                self.present_mode_message(_("blank"))
            return
        if role_prefix:
            line_text = self._normalize_diff_line_content(line_text, role)
            if line_text.strip() == "":
                self.present_mode_message(role_prefix + ": " + _("blank"))
                return
            if role in {"added_line", "removed_line"} and line_text in {
                _("added"),
                _("removed"),
            }:
                self.present_mode_message(line_text)
                return
            self.present_mode_message(role_prefix + ": " + line_text)
        else:
            self.present_mode_message(line_text)

    def _format_classic_diff_command(self, line_text):
        match = re.match(r"^(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?$", line_text.strip())
        if not match:
            return None

        old_start = int(match.group(1))
        old_end = int(match.group(2) or match.group(1))
        op = match.group(3)
        new_start = int(match.group(4))
        new_end = int(match.group(5) or match.group(4))

        if op == "c":
            return self._format_line_span(old_start, old_end) + " " + _("changed")
        if op == "d":
            return self._format_line_span(old_start, old_end) + " " + _("deleted")
        if op == "a":
            return self._format_line_span(new_start, new_end) + " " + _("added")
        return None

    def _format_line_span(self, start_line, end_line):
        if start_line == end_line:
            return _("line ") + str(start_line)
        return (
            _("lines ")
            + str(start_line)
            + _(" through ")
            + str(end_line)
        )

    def _announce_current_char(self):
        if self.current_line < 0 or self.current_line >= len(self.lines):
            self.present_mode_message(_("No diff file loaded."))
            return
        line_text = self.lines[self.current_line]
        if len(line_text) == 0:
            self.present_mode_message(_("blank"))
            return
        if self.current_char < 0:
            self.current_char = 0
        if self.current_char >= len(line_text):
            self.current_char = len(line_text) - 1
        char_value = line_text[self.current_char]
        if char_value == " ":
            self.present_mode_message(_("space"))
            return
        self.env["runtime"]["OutputManager"].present_text(
            char_value,
            interrupt=True,
            ignore_punctuation=True,
            announce_capital=True,
        )

    def _announce_word_at_current_char(self):
        if self.current_line < 0 or self.current_line >= len(self.lines):
            self.present_mode_message(_("No diff file loaded."))
            return
        line_text = self.lines[self.current_line]
        for start, end in self._get_word_spans(line_text):
            if start <= self.current_char < end:
                self.present_mode_message(line_text[start:end])
                return
        self.present_mode_message(_("No word at current position."))

    def _ensure_current_line(self):
        if not self.has_loaded_diff():
            self.present_mode_message(_("No diff file loaded."))
            return False
        if self.current_line < 0:
            self.current_line = 0
            self.current_char = 0
        if self.current_line >= len(self.lines):
            self.current_line = len(self.lines) - 1
            self.current_char = 0
        return True

    def _get_word_spans(self, line_text):
        return [match.span() for match in re.finditer(r"\S+", line_text)]

    def _announce_file_section(self, line_index, line_meta):
        file_index = line_meta["file_index"]
        if file_index == -1 or file_index >= len(self.file_sections):
            self.present_mode_message(_("File section."))
            return
        file_section = self.file_sections[file_index]
        message = (
            _("File section ")
            + str(file_index + 1)
            + _(" of ")
            + str(len(self.file_sections))
            + _(". Plus ")
            + str(file_section["added_count"])
            + _(", minus ")
            + str(file_section["removed_count"])
            + "."
        )
        self.present_mode_message(message)

    def _announce_hunk_section(self, line_index, line_meta, line_text):
        hunk_index = line_meta["hunk_index"]
        if hunk_index == -1 or hunk_index >= len(self.hunk_sections):
            self.present_mode_message(_("Hunk section."))
            return

        hunk_section = self.hunk_sections[hunk_index]
        file_index = hunk_section["file_index"]
        file_position = file_index + 1 if file_index >= 0 else 0
        hunk_position = hunk_section["position_in_file"]
        hunk_total = hunk_section["hunks_in_file"]

        message = (
            _("Hunk ")
            + str(hunk_position)
            + _(" of ")
            + str(hunk_total)
            + _(" in file ")
            + str(file_position)
            + _(" of ")
            + str(len(self.file_sections))
            + _(". Plus ")
            + str(hunk_section["added_count"])
            + _(", minus ")
            + str(hunk_section["removed_count"])
            + "."
        )

        if self._get_diff_verbosity() == "verbose":
            message += " " + self._line_content_for_speech(line_text, "hunk_header")

        self.present_mode_message(message)

    def _line_content_for_speech(self, line_text, role):
        line_text = self._normalize_diff_line_content(line_text, role)
        if line_text.strip() == "":
            return _("blank")
        return line_text

    def _normalize_diff_line_content(self, line_text, role):
        if role == "added_line":
            stripped_line = line_text.lstrip("+")
            if stripped_line.strip() == "" and line_text.startswith("+"):
                return _("added")
            return stripped_line
        if role == "removed_line":
            stripped_line = line_text.lstrip("-")
            if stripped_line.strip() == "" and line_text.startswith("-"):
                return _("removed")
            return stripped_line
        return line_text

    def _build_location_text(self, line_index):
        if line_index < 0 or line_index >= len(self.line_meta):
            return ""
        line_meta = self.line_meta[line_index]
        file_index = line_meta["file_index"]
        hunk_index = line_meta["hunk_index"]
        location_parts = []

        if file_index != -1 and file_index < len(self.file_sections):
            location_parts.append(
                _("File ")
                + str(file_index + 1)
                + _(" of ")
                + str(len(self.file_sections))
                + ".")

        if hunk_index != -1 and hunk_index < len(self.hunk_sections):
            hunk_info = self.hunk_sections[hunk_index]
            location_parts.append(
                _("Hunk ")
                + str(hunk_info["position_in_file"])
                + _(" of ")
                + str(hunk_info["hunks_in_file"])
                + "."
            )

        return " ".join(location_parts)

    def _get_diff_verbosity(self):
        verbosity = self.env["runtime"]["SettingsManager"].get_setting(
            "general", "diff_verbosity"
        ).lower()
        if verbosity not in {"compact", "verbose"}:
            return "compact"
        return verbosity

    def present_error(self, message):
        self.present_mode_message(message)

    def present_mode_message(self, message):
        self.env["runtime"]["OutputManager"].present_text(
            message,
            interrupt=True,
        )

    def _install_diff_bindings(self):
        self.bindings_backup = self.env["bindings"].copy()
        self.raw_bindings_backup = self.env["rawBindings"].copy()

        diff_bindings = {
            str([1, ["KEY_H"]]): "DIFF_NEXT_HUNK",
            str([1, sorted(["KEY_SHIFT", "KEY_H"])]): "DIFF_PREV_HUNK",
            str([1, ["KEY_A"]]): "DIFF_NEXT_ADDED",
            str([1, sorted(["KEY_SHIFT", "KEY_A"])]): "DIFF_PREV_ADDED",
            str([1, ["KEY_D"]]): "DIFF_NEXT_REMOVED",
            str([1, sorted(["KEY_SHIFT", "KEY_D"])]): "DIFF_PREV_REMOVED",
            str([1, ["KEY_F"]]): "DIFF_NEXT_FILE_SECTION",
            str([1, sorted(["KEY_SHIFT", "KEY_F"])]): "DIFF_PREV_FILE_SECTION",
            str([1, ["KEY_S"]]): "DIFF_SUMMARY",
            str([1, ["KEY_F1"]]): "DIFF_KEY_HELP",
            str([1, ["KEY_UP"]]): "DIFF_PREV_LINE",
            str([1, ["KEY_DOWN"]]): "DIFF_NEXT_LINE",
            str([1, ["KEY_LEFT"]]): "DIFF_PREV_CHAR",
            str([1, ["KEY_RIGHT"]]): "DIFF_NEXT_CHAR",
            str([1, sorted(["KEY_CTRL", "KEY_LEFT"])]): "DIFF_PREV_WORD",
            str([1, sorted(["KEY_CTRL", "KEY_RIGHT"])]): "DIFF_NEXT_WORD",
            str([1, ["KEY_ESC"]]): "EXIT_DIFF_MODE",
            str([1, sorted(["KEY_FENRIR", "KEY_CTRL", "KEY_D"])]): "TOGGLE_DIFF_MODE",
        }

        diff_raw_bindings = {
            shortcut: [1, keys]
            for shortcut, keys in [
                (str([1, ["KEY_H"]]), ["KEY_H"]),
                (str([1, sorted(["KEY_SHIFT", "KEY_H"])]), sorted(["KEY_SHIFT", "KEY_H"])),
                (str([1, ["KEY_A"]]), ["KEY_A"]),
                (str([1, sorted(["KEY_SHIFT", "KEY_A"])]), sorted(["KEY_SHIFT", "KEY_A"])),
                (str([1, ["KEY_D"]]), ["KEY_D"]),
                (str([1, sorted(["KEY_SHIFT", "KEY_D"])]), sorted(["KEY_SHIFT", "KEY_D"])),
                (str([1, ["KEY_F"]]), ["KEY_F"]),
                (str([1, sorted(["KEY_SHIFT", "KEY_F"])]), sorted(["KEY_SHIFT", "KEY_F"])),
                (str([1, ["KEY_S"]]), ["KEY_S"]),
                (str([1, ["KEY_F1"]]), ["KEY_F1"]),
                (str([1, ["KEY_UP"]]), ["KEY_UP"]),
                (str([1, ["KEY_DOWN"]]), ["KEY_DOWN"]),
                (str([1, ["KEY_LEFT"]]), ["KEY_LEFT"]),
                (str([1, ["KEY_RIGHT"]]), ["KEY_RIGHT"]),
                (
                    str([1, sorted(["KEY_CTRL", "KEY_LEFT"])]),
                    sorted(["KEY_CTRL", "KEY_LEFT"]),
                ),
                (
                    str([1, sorted(["KEY_CTRL", "KEY_RIGHT"])]),
                    sorted(["KEY_CTRL", "KEY_RIGHT"]),
                ),
                (str([1, ["KEY_ESC"]]), ["KEY_ESC"]),
                (
                    str([1, sorted(["KEY_FENRIR", "KEY_CTRL", "KEY_D"])]),
                    sorted(["KEY_FENRIR", "KEY_CTRL", "KEY_D"]),
                ),
            ]
        }

        self.env["bindings"] = diff_bindings
        self.env["rawBindings"] = diff_raw_bindings

    def _restore_bindings(self):
        if self.bindings_backup is not None:
            self.env["bindings"] = self.bindings_backup
        if self.raw_bindings_backup is not None:
            self.env["rawBindings"] = self.raw_bindings_backup
        self.bindings_backup = None
        self.raw_bindings_backup = None

    def _write_debug(self, message, level):
        self.env["runtime"]["DebugManager"].write_debug_out(
            message, level
        )
