"""Miscellaneous dialogs."""

import tkinter as tk
from tkinter import ttk, font
from typing import Any
import sys
import unicodedata

import regex as re

from guiguts.maintext import maintext
from guiguts.preferences import (
    PrefKey,
    PersistentBoolean,
    PersistentInt,
    PersistentString,
    preferences,
)
from guiguts.root import root
from guiguts.utilities import is_mac, is_windows, is_x11, sound_bell
from guiguts.widgets import (
    ToplevelDialog,
    ToolTip,
    insert_in_focus_widget,
    OkApplyCancelDialog,
    mouse_bind,
    Combobox,
)

COMBO_SEPARATOR = "―" * 20


class PreferencesDialog(ToplevelDialog):
    """A dialog that displays settings/preferences."""

    manual_page = "Edit_Menu#Preferences"

    def __init__(self) -> None:
        """Initialize preferences dialog."""
        super().__init__("Settings", resize_x=False, resize_y=False)
        self.minsize(250, 10)

        # Appearance
        appearance_frame = ttk.LabelFrame(self.top_frame, text="Appearance", padding=10)
        appearance_frame.grid(column=0, row=0, sticky="NSEW")
        theme_frame = ttk.Frame(appearance_frame)
        theme_frame.grid(column=0, row=0, sticky="NSEW")
        theme_frame.columnconfigure(1, weight=1)
        ttk.Label(theme_frame, text="Theme (change requires restart): ").grid(
            column=0, row=0, sticky="NE"
        )
        cb = ttk.Combobox(
            theme_frame, textvariable=PersistentString(PrefKey.THEME_NAME)
        )
        cb.grid(column=1, row=0, sticky="NEW")
        cb["values"] = ["Default", "Dark", "Light"]
        cb["state"] = "readonly"
        tearoff_check = ttk.Checkbutton(
            appearance_frame,
            text="Use Tear-Off Menus (change requires restart)",
            variable=PersistentBoolean(PrefKey.TEAROFF_MENUS),
        )
        tearoff_check.grid(column=0, row=1, sticky="NEW", pady=5)
        if is_mac():
            tearoff_check["state"] = tk.DISABLED
            ToolTip(tearoff_check, "Not available on macOS")

        # Font
        def is_valid_font(new_value: str) -> bool:
            """Validation routine for Combobox - if separator has been selected,
            select Courier New instead.

            Args:
                new_value: New font family selected by user.

            Returns:
                True, because if invalid, it is fixed in this routine.
            """
            if new_value == COMBO_SEPARATOR:
                preferences.set(PrefKey.TEXT_FONT_FAMILY, "Courier")
                preferences.set(PrefKey.TEXT_FONT_FAMILY, "Courier New")
            return True

        font_frame = ttk.Frame(appearance_frame)
        font_frame.grid(column=0, row=3, sticky="NEW", pady=(5, 0))
        ttk.Label(font_frame, text="Font: ").grid(column=0, row=0, sticky="NEW")
        font_list = sorted(font.families(), key=str.lower)
        font_list.insert(0, COMBO_SEPARATOR)
        for preferred_font in "Courier New", "DejaVu Sans Mono", "DP Sans Mono":
            if preferred_font in font_list:
                font_list.insert(0, preferred_font)
            elif preferred_font == "Courier New" and "Courier" in font_list:
                font_list.insert(0, "Courier")
        cb = ttk.Combobox(
            font_frame,
            textvariable=PersistentString(PrefKey.TEXT_FONT_FAMILY),
            width=30,
            validate=tk.ALL,
            validatecommand=(self.register(is_valid_font), "%P"),
            values=font_list,
            state="readonly",
        )
        cb.grid(column=1, row=0, sticky="NEW")

        spinbox = ttk.Spinbox(
            font_frame,
            textvariable=PersistentInt(PrefKey.TEXT_FONT_SIZE),
            from_=1,
            to=99,
            width=5,
        )
        spinbox.grid(column=2, row=0, sticky="NEW", padx=2)
        ToolTip(spinbox, "Font size")

        ttk.Checkbutton(
            appearance_frame,
            text="Display Line Numbers",
            variable=PersistentBoolean(PrefKey.LINE_NUMBERS),
        ).grid(column=0, row=4, sticky="NEW", pady=5)
        ttk.Checkbutton(
            appearance_frame,
            text="Display Column Numbers",
            variable=PersistentBoolean(PrefKey.COLUMN_NUMBERS),
        ).grid(column=0, row=5, sticky="NEW", pady=5)
        ttk.Checkbutton(
            appearance_frame,
            text="Show Character Names in Status Bar",
            variable=root().ordinal_names_state,
        ).grid(column=0, row=6, sticky="NEW", pady=5)
        bell_frame = ttk.Frame(appearance_frame)
        bell_frame.grid(column=0, row=7, sticky="NEW", pady=(5, 0))
        ttk.Label(bell_frame, text="Warning bell: ").grid(column=0, row=0, sticky="NEW")
        ttk.Checkbutton(
            bell_frame,
            text="Audible",
            variable=PersistentBoolean(PrefKey.BELL_AUDIBLE),
        ).grid(column=1, row=0, sticky="NEW", padx=20)
        ttk.Checkbutton(
            bell_frame,
            text="Visual",
            variable=PersistentBoolean(PrefKey.BELL_VISUAL),
        ).grid(column=2, row=0, sticky="NEW")

        iv_btn = ttk.Checkbutton(
            appearance_frame,
            text="Image Viewer Alert",
            variable=PersistentBoolean(PrefKey.IMAGE_VIEWER_ALERT),
        )
        iv_btn.grid(column=0, row=8, sticky="NEW", pady=5)
        ToolTip(
            iv_btn,
            "Whether to flash the border when Auto Img re-loads the\n"
            "default image after you manually select a different image",
        )

        # Wrapping tab
        wrapping_frame = ttk.LabelFrame(self.top_frame, text="Wrapping", padding=10)
        wrapping_frame.grid(column=0, row=1, sticky="NSEW", pady=(10, 0))

        def add_label_spinbox(row: int, label: str, key: PrefKey, tooltip: str) -> None:
            """Add a label and spinbox to the wrapping frame.
            Args:
                label: Text for label.
                key: Prefs key to use to store preference.
                tooltip: Text for tooltip.
            """
            ttk.Label(wrapping_frame, text=label).grid(
                column=0, row=row, sticky="NSE", pady=2
            )
            spinbox = ttk.Spinbox(
                wrapping_frame,
                textvariable=PersistentInt(key),
                from_=0,
                to=999,
                width=5,
            )
            spinbox.grid(column=1, row=row, sticky="NW", padx=5, pady=2)
            ToolTip(spinbox, tooltip)

        add_label_spinbox(
            0, "Left Margin:", PrefKey.WRAP_LEFT_MARGIN, "Left margin for normal text"
        )
        add_label_spinbox(
            1,
            "Right Margin:",
            PrefKey.WRAP_RIGHT_MARGIN,
            "Right margin for normal text",
        )
        add_label_spinbox(
            2,
            "Blockquote Indent:",
            PrefKey.WRAP_BLOCKQUOTE_INDENT,
            "Extra indent for each level of /# blockquotes",
        )
        add_label_spinbox(
            3,
            "Blockquote Right Margin:",
            PrefKey.WRAP_BLOCKQUOTE_RIGHT_MARGIN,
            "Right margin for /# blockquotes",
        )
        add_label_spinbox(
            4,
            "Nowrap Block Indent:",
            PrefKey.WRAP_BLOCK_INDENT,
            "Indent for /* and /L blocks",
        )
        add_label_spinbox(
            5,
            "Poetry Indent:",
            PrefKey.WRAP_POETRY_INDENT,
            "Indent for /P poetry blocks",
        )
        add_label_spinbox(
            6,
            "Index Main Entry Margin:",
            PrefKey.WRAP_INDEX_MAIN_MARGIN,
            "Indent for main entries in index - sub-entries retain their indent relative to this",
        )
        add_label_spinbox(
            8,
            "Index Wrap Margin:",
            PrefKey.WRAP_INDEX_WRAP_MARGIN,
            "Left margin for all lines rewrapped in index",
        )
        add_label_spinbox(
            9,
            "Index Right Margin:",
            PrefKey.WRAP_INDEX_RIGHT_MARGIN,
            "Right margin for index entries",
        )


_compose_dict: dict[str, str] = {}


class ComposeSequenceDialog(OkApplyCancelDialog):
    """Dialog to enter Compose Sequences.

    Attributes:
        dict: Dictionary mapping sequence of keystrokes to character.
    """

    manual_page = "Tools_Menu#Compose_Sequence"

    def __init__(self) -> None:
        """Initialize compose sequence dialog."""
        super().__init__("Compose Sequence", resize_x=False, resize_y=False)
        ttk.Label(self.top_frame, text="Compose: ").grid(column=0, row=0, sticky="NSEW")
        self.string = tk.StringVar()
        self.entry = Combobox(
            self.top_frame, PrefKey.COMPOSE_HISTORY, textvariable=self.string
        )
        # self.entry = ttk.Entry(self.top_frame, textvariable=self.string, name="entry1")
        self.entry.grid(column=1, row=0, sticky="NSEW")
        # In tkinter, binding order is widget, class, toplevel, all
        # Swap first two, so that class binding has time to set textvariable
        # before the widget binding below is executed.
        bindings = self.entry.bindtags()
        self.entry.bindtags((bindings[1], bindings[0], bindings[2], bindings[3]))
        self.entry.bind("<Key>", lambda _event: self.interpret_and_insert())
        self.entry.focus()
        init_compose_dict()

    def apply_changes(self) -> bool:
        """Overridden function called when Apply/OK buttons are pressed.

        Call to attempt to interpret compose sequence

        Returns:
            Always returns True, meaning OK button (or Return key) will close dialog.
        """
        self.interpret_and_insert(force=True)
        self.entry.select_range(0, tk.END)
        return True

    def interpret_and_insert(self, force: bool = False) -> None:
        """Interpret string from Entry field as a compose sequence and insert it.

        If compose sequence is complete, then the composed character will be
        inserted in the most recently focused text/entry widget and the Compose
        dialog will be closed. If sequence is not complete, then nothing will be done,
        unless `force` is True.

        Args:
            force: True if string should be interpreted/inserted as-is,
                rather than waiting for further input.
        """
        sequence = self.string.get()
        char = ""
        # First check if sequence is in dictionary
        if sequence in _compose_dict:
            char = _compose_dict[sequence]
        elif match := re.fullmatch(r"[0-9a-f]{4}", sequence, re.IGNORECASE):
            # Exactly 4 hex digits translates to a single Unicode character
            char = chr(int(sequence, 16))
        elif force:
            if match := re.fullmatch(
                r"(0x|\\x|x|U\+?)?([0-9a-fA-F]{2,})", sequence, re.IGNORECASE
            ):
                # Or user can force interpretation as hex with fewer than 4 digits,
                # or with more than 4 by using a prefix: 0x, \x, x, U or U+
                char = chr(int(match[2], 16))
            elif match := re.fullmatch(r"#(\d{2,})", sequence):
                # Or specify in decimal following '#' character
                char = chr(int(match[1]))
        # Don't insert anything if no match
        if not char:
            return
        insert_in_focus_widget(char)
        self.entry.add_to_history(self.string.get())
        if not force:
            self.destroy()


def init_compose_dict() -> None:
    """Initialize dictionary of compose sequences."""
    if _compose_dict:
        return  # Already initialized
    init_char_reversible("‘", "'<")
    init_char_reversible("’", "'>")
    init_char_reversible("“", '"<')
    init_char_reversible("”", '">')
    init_chars("±", "+-")
    init_chars("·", "^.", "*.", ".*")
    init_chars("×", "*x", "x*")
    init_chars("÷", ":-")
    init_char_reversible("°", "oo", "*o")
    init_char_reversible("′", "1'")
    init_char_reversible("″", "2'")
    init_char_reversible("‴", "3'")
    init_char_reversible(" ", "  ", "* ")
    init_chars("—", "--")
    init_char_reversible("–", "- ")
    init_chars("⁂", "**")
    init_char_reversible("º", "o_")
    init_char_reversible("ª", "a_")
    init_chars("‖", "||")
    init_chars("¡", "!!")
    init_chars("¿", "??")
    init_chars("«", "<<")
    init_chars("»", ">>")
    init_char_case("Æ", "AE")
    init_char_case("Œ", "OE")
    init_char_case("ẞ", "SS")
    init_char_case("Ð", "DH", "ETH")
    init_char_case("Þ", "TH")
    init_chars("©", "(c)", "(C)")
    init_chars("†", "dag", "DAG")
    init_chars("‡", "ddag", "DDAG")
    init_accent("£", "L", "-")
    init_accent("¢", "C", "/", "|")
    init_chars("§", "sec", "s*", "*s", "SEC", "S*", "*S")
    init_chars("¶", "pil", "p*", "*p", "PIL", "P*", "*P")
    init_chars("ſ", "sf", "SF")
    init_chars("‚", ",'")
    init_chars("‛", "^'")
    init_chars("„", ',"')
    init_chars("‟", '^"')
    init_chars("½", "1/2")
    init_chars("⅓", "1/3")
    init_chars("⅔", "2/3")
    init_chars("¼", "1/4")
    init_chars("¾", "3/4")
    init_chars("⅕", "1/5")
    init_chars("⅖", "2/5")
    init_chars("⅗", "3/5")
    init_chars("⅘", "4/5")
    init_chars("⅙", "1/6")
    init_chars("⅚", "5/6")
    init_chars("⅐", "1/7")
    init_chars("⅛", "1/8")
    init_chars("⅜", "3/8")
    init_chars("⅝", "5/8")
    init_chars("⅞", "7/8")
    init_chars("⅑", "1/9")
    init_chars("⅒", "1/10")
    for num, char in enumerate("⁰¹²³⁴⁵⁶⁷⁸⁹"):
        init_chars(char, f"^{num}")
    for num, char in enumerate("₀₁₂₃₄₅₆₇₈₉"):
        init_chars(char, f",{num}")

    # Accented characters
    init_accent("À", "A", "`", "\\")
    init_accent("Á", "A", "'", "/")
    init_accent("Â", "A", "^")
    init_accent("Ã", "A", "~")
    init_accent("Ä", "A", '"', ":")
    init_accent("Å", "A", "o", "*")
    init_accent("Ā", "A", "-", "=")
    init_accent("È", "E", "`", "\\")
    init_accent("É", "E", "'", "/")
    init_accent("Ê", "E", "^")
    init_accent("Ë", "E", '"', ":")
    init_accent("Ē", "E", "-", "=")
    init_accent("Ì", "I", "`", "\\")
    init_accent("Í", "I", "'", "/")
    init_accent("Î", "I", "^")
    init_accent("Ï", "I", '"', ":")
    init_accent("Ī", "I", "-", "=")
    init_accent("Ò", "O", "`", "\\")
    init_accent("Ó", "O", "'")
    init_accent("Ô", "O", "^")
    init_accent("Õ", "O", "~")
    init_accent("Ö", "O", '"', ":")
    init_accent("Ø", "O", "/")
    init_accent("Ō", "O", "-", "=")
    init_accent("Ù", "U", "`", "\\")
    init_accent("Ú", "U", "'", "/")
    init_accent("Û", "U", "^")
    init_accent("Ü", "U", '"', ":")
    init_accent("Ū", "U", "-", "=")
    init_accent("Ç", "C", ",")
    init_accent("Ñ", "N", "~")
    init_accent("Ÿ", "Y", '"', ":")
    init_accent("Ý", "Y", "'", "/")

    # Combining characters
    init_combining("\u0300", "\u0316", "\\", "`")  # grave
    init_combining("\u0301", "\u0317", "/", "'")  # acute
    init_combining("\u0302", "\u032D", "^")  # circumflex
    init_combining("\u0303", "\u0330", "~")  # tilde
    init_combining("\u0304", "\u0331", "-", "=")  # macron
    init_combining("\u0306", "\u032E", ")")  # breve
    init_combining("\u0311", "\u032F", "(")  # inverted breve
    init_combining("\u0307", "\u0323", ".")  # dot
    init_combining("\u0308", "\u0324", ":", '"')  # diaresis
    init_combining("\u0309", "", "?")  # hook above
    init_combining("\u030A", "\u0325", "*")  # ring
    init_combining("\u030C", "\u032C", "v")  # caron
    init_combining("", "\u0327", ",")  # cedilla
    init_combining("", "\u0328", ";")  # ogonek

    # Greek characters
    init_greek_alphabet()
    init_greek_accent("Ὰ", "A", "ᾼ")
    init_greek_accent("Ὲ", "E")
    init_greek_accent("Ὴ", "H", "ῌ")
    init_greek_accent("Ὶ", "I")
    init_greek_accent("Ὸ", "O")
    init_greek_accent("Ὺ", "U")
    init_greek_accent("Ὼ", "W", "ῼ")
    init_greek_breathing("Ἀ", "A", "ᾈ")
    init_greek_breathing("Ἐ", "E")
    init_greek_breathing("Ἠ", "H", "ᾘ")
    init_greek_breathing("Ἰ", "I")
    init_greek_breathing("Ὀ", "O")
    init_greek_breathing("὘", "U")
    init_greek_breathing("Ὠ", "W", "ᾨ")


def init_accent(char: str, base: str, *accents: str) -> None:
    """Add entries to the dictionary for upper & lower case versions
    of the given char, using each of the accents.

    Args:
        char: Upper case version of character to be added.
        base: Upper case base English character to be accented.
        *accents: Characters that can be used to add the accent.
    """
    for accent in accents:
        init_char_case(char, base + accent)
        init_char_case(char, accent + base)


def init_chars(char: str, *sequences: str) -> None:
    """Add entries to the dictionary for the given char.

    Args:
        char: Character to be added.
        *sequences: Sequences of keys to generate the character.
    """
    for sequence in sequences:
        _compose_dict[sequence] = char


def init_char_reversible(char: str, *sequences: str) -> None:
    """Add entries to the dictionary for the given char with
    2 reversible characters per sequence.

    Args:
        char: Character to be added.
        *sequences: Sequences of reversible keys to generate the character.
    """
    for sequence in sequences:
        _compose_dict[sequence] = char
        _compose_dict[sequence[::-1]] = char


def init_char_case(char: str, *sequences: str) -> None:
    """Add upper & lower case entries to the dictionary for the given char & sequence.

    Args:
        char: Character to be added.
        *sequences: Sequences of keys to generate the character.
    """
    lchar = char.lower()
    for sequence in sequences:
        lsequence = sequence.lower()
        _compose_dict[sequence] = char
        _compose_dict[lsequence] = lchar


def init_combining(above: str, below: str, *accents: str) -> None:
    """Add entries to the dictionary for combining characters.

    Args:
        above: Combining character above (empty if none to be added).
        base: Combining character below (empty if none to be added).
        *accents: Characters that follow `+` and `_` to create the combining characters.
    """
    for accent in accents:
        if above:
            _compose_dict["+" + accent] = above
        if below:
            _compose_dict["_" + accent] = below


def init_greek_alphabet() -> None:
    """Add entries to the dictionary for non-accented Greek alphabet letters.

    Greek letter sequences are prefixed with `=` sign.
    """
    ualpha = ord("Α")
    lalpha = ord("α")
    for offset, base in enumerate("ABGDEZHQIKLMNXOPRJSTUFCYW"):
        _compose_dict["=" + base] = chr(ualpha + offset)
        _compose_dict["=" + base.lower()] = chr(lalpha + offset)


def init_greek_accent(char: str, base: str, uiota: str = "") -> None:
    """Add varia & oxia accented Greek letters to dictionary.

    Greek letter sequences are prefixed with `=` sign.

    Args:
        char: Upper case version of character with varia to be added.
            Next ordinal gives the character with oxia.
        base: Upper case base English character to be accented.
        uiota: Optional upper case version of character with iota subscript.
            Prev & next ordinals give same with accents for lower case only.
            Note there is no upper case with accent and iota, and not all
            vowels have an iota version.
    """
    _compose_dict["=\\" + base] = _compose_dict["=`" + base] = char
    _compose_dict["=/" + base] = _compose_dict["='" + base] = chr(ord(char) + 1)
    lbase = base.lower()
    lchar = char.lower()
    _compose_dict["=\\" + lbase] = _compose_dict["=`" + lbase] = lchar
    _compose_dict["=/" + lbase] = _compose_dict["='" + lbase] = chr(ord(lchar) + 1)
    if not uiota:
        return
    _compose_dict["=|" + base] = uiota
    liota = uiota.lower()
    _compose_dict["=|" + lbase] = liota
    _compose_dict["=\\|" + lbase] = _compose_dict["=`|" + lbase] = chr(ord(liota) - 1)
    _compose_dict["=/|" + lbase] = _compose_dict["='|" + lbase] = chr(ord(liota) + 1)


def init_greek_breathing(char: str, base: str, uiota: str = "") -> None:
    """Add accented Greek letters including breathing to dictionary.

    Greek letter sequences are prefixed with `=` sign.

    Args:
        char: Upper case char in middle group of 16, e.g. alpha with various accents.
        base: Upper case base English character to be accented.
        iota: Optional upper case verison with iota subscript if needed.
    """
    lbase = base.lower()
    ord_list = (ord(char), ord(uiota)) if uiota else (ord(char),)
    for char_ord in ord_list:
        _compose_dict["=)" + base] = chr(char_ord)
        _compose_dict["=(" + base] = chr(char_ord + 1)
        _compose_dict["=(`" + base] = _compose_dict["=(\\" + base] = chr(char_ord + 2)
        _compose_dict["=)`" + base] = _compose_dict["=)\\" + base] = chr(char_ord + 3)
        _compose_dict["=('" + base] = _compose_dict["=(/" + base] = chr(char_ord + 4)
        _compose_dict["=)'" + base] = _compose_dict["=)/" + base] = chr(char_ord + 5)
        _compose_dict["=(^" + base] = _compose_dict["=(~" + base] = chr(char_ord + 6)
        _compose_dict["=)^" + base] = _compose_dict["=)~" + base] = chr(char_ord + 7)
        _compose_dict["=)" + lbase] = chr(char_ord - 8)
        _compose_dict["=(" + lbase] = chr(char_ord - 7)
        _compose_dict["=(`" + lbase] = _compose_dict["=(\\" + lbase] = chr(char_ord - 6)
        _compose_dict["=)`" + lbase] = _compose_dict["=)\\" + lbase] = chr(char_ord - 5)
        _compose_dict["=('" + lbase] = _compose_dict["=(/" + lbase] = chr(char_ord - 4)
        _compose_dict["=)'" + lbase] = _compose_dict["=)/" + lbase] = chr(char_ord - 3)
        _compose_dict["=(^" + lbase] = _compose_dict["=(~" + lbase] = chr(char_ord - 2)
        _compose_dict["=)^" + lbase] = _compose_dict["=)~" + base] = chr(char_ord - 1)
        # Add iota sequence character in case going round loop a second time
        base = "|" + base
        lbase = "|" + lbase


class ComposeHelpDialog(ToplevelDialog):
    """A dialog to show the compose sequences."""

    manual_page = "Tools_Menu#Compose_Sequence"

    def __init__(self) -> None:
        """Initialize class members from page details."""
        super().__init__("List of Compose Sequences")

        self.column_headings = ("Character", "Sequence", "Name")
        widths = (70, 70, 600)
        self.help = ttk.Treeview(
            self.top_frame,
            columns=self.column_headings,
            show="headings",
            height=10,
            selectmode=tk.NONE,
        )
        ToolTip(
            self.help,
            "Click to insert character",
            use_pointer_pos=True,
        )
        for col, column in enumerate(self.column_headings):
            self.help.column(
                f"#{col + 1}",
                minwidth=10,
                width=widths[col],
                stretch=False,
                anchor=tk.W,
            )
            self.help.heading(
                f"#{col + 1}",
                text=column,
                anchor=tk.W,
            )
        self.help.grid(row=0, column=0, sticky=tk.NSEW)

        self.scrollbar = ttk.Scrollbar(
            self.top_frame, orient=tk.VERTICAL, command=self.help.yview
        )
        self.help.configure(yscroll=self.scrollbar.set)  # type: ignore[call-overload]
        self.scrollbar.grid(row=0, column=1, sticky=tk.NS)

        mouse_bind(self.help, "1", self.insert_char)

        init_compose_dict()

        # Avoid displaying help for reversed 2-char sequence, e.g. "o*" and "*o"
        # Remember ones that have been entered already
        reverse_done = {}
        for sequence, char in _compose_dict.items():
            seq_display = sequence.replace(" ", "␣")
            if len(sequence) == 2:
                rev_sequence = sequence[::-1]
                if rev_sequence in reverse_done:
                    continue
                rev_char = _compose_dict.get(rev_sequence)
                if rev_char == char and rev_sequence != sequence:
                    seq_display += f"  or  {rev_sequence.replace(' ', '␣')}"
                    reverse_done[sequence] = char
            # Don't add uppercase version if it leads to identical character, e.g. "dag"/"DAG" for dagger
            if (
                char == _compose_dict.get(sequence.lower())
                and sequence != sequence.lower()
            ):
                continue
            try:
                name = unicodedata.name(char)
            except ValueError:
                continue  # Some Greek combinations don't exist
            entry = (char, seq_display, name)
            self.help.insert("", tk.END, values=entry)

        children = self.help.get_children()
        if children:
            self.help.see(children[0])

    def insert_char(self, event: tk.Event) -> None:
        """Insert character corresponding to row clicked.

        Args:
            event: Event containing location of mouse click
        """
        row_id = self.help.identify_row(event.y)
        row = self.help.set(row_id)
        try:
            char = row[self.column_headings[0]]
        except KeyError:
            return
        insert_in_focus_widget(char)


class ScrollableFrame(ttk.Frame):
    """A scrollable ttk.Frame.

    Consider rewriting, so it's like ScrolledReadOnlyText, i.e. self is the frame you add to,
    and grid method is overridden for correct placement.
    """

    def __init__(self, container: tk.Widget, *args: Any, **kwargs: Any) -> None:
        """Initialize ScrollableFrame."""
        super().__init__(container, *args, **kwargs)
        self.canvas = tk.Canvas(self, highlightthickness=0)
        v_scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        h_scrollbar = ttk.Scrollbar(
            self, orient="horizontal", command=self.canvas.xview
        )
        self.scrollable_frame = ttk.Frame(self.canvas)
        bg = str(ttk.Style().lookup(self["style"], "background"))
        self.canvas["background"] = bg

        self.scrollable_frame.bind(
            "<Configure>",
            lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")),
        )
        self.scrollable_frame.bind("<Enter>", self.on_enter)
        self.scrollable_frame.bind("<Leave>", self.on_leave)

        self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
        self.canvas.configure(yscrollcommand=v_scrollbar.set)
        self.canvas.configure(xscrollcommand=h_scrollbar.set)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.canvas.grid(column=0, row=0, sticky="NSEW")
        v_scrollbar.grid(column=1, row=0, sticky="NSEW")
        h_scrollbar.grid(column=0, row=1, sticky="NSEW")

    def on_mouse_wheel(self, event: tk.Event) -> None:
        """Cross platform scroll wheel event."""
        if is_windows():
            self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        elif is_mac():
            self.canvas.yview_scroll(int(-1 * event.delta), "units")
        else:
            if event.num == 4:
                self.canvas.yview_scroll(-1, "units")
            elif event.num == 5:
                self.canvas.yview_scroll(1, "units")

    def on_enter(self, _event: tk.Event) -> None:
        """Bind wheel events when the cursor enters the control."""
        if is_x11():
            self.canvas.bind_all("<Button-4>", self.on_mouse_wheel)
            self.canvas.bind_all("<Button-5>", self.on_mouse_wheel)
        else:
            self.canvas.bind_all("<MouseWheel>", self.on_mouse_wheel)

    def on_leave(self, _event: tk.Event) -> None:
        """Unbind wheel events when the cursorl leaves the control."""
        if is_x11():
            self.canvas.unbind_all("<Button-4>")
            self.canvas.unbind_all("<Button-5>")
        else:
            self.canvas.unbind_all("<MouseWheel>")

    def reset_scroll(self) -> None:
        """Scroll canvas to top left position."""
        self.canvas.xview_moveto(0.0)
        self.canvas.yview_moveto(0.0)


class UnicodeBlockDialog(ToplevelDialog):
    """A dialog that displays a block of Unicode characters, and allows
    the user to click on them to insert them into text window."""

    manual_page = "Tools_Menu#Unicode_Blocks"
    commonly_used_characters_name = "Commonly Used Characters"

    def __init__(self) -> None:
        """Initialize Unicode Block dialog."""

        super().__init__("Unicode Block")

        cb = ttk.Combobox(
            self.top_frame,
            textvariable=PersistentString(PrefKey.UNICODE_BLOCK),
            width=50,
        )
        cb.grid(column=0, row=0, sticky="NSW", padx=5, pady=(5, 0))
        block_list = []
        for name, (beg, end, show) in _unicode_blocks.items():
            if show:
                block_list.append(f"{name}   ({beg:04X}–{end:04X})")
        block_list.sort()
        block_list.insert(0, UnicodeBlockDialog.commonly_used_characters_name)
        cb["values"] = block_list
        cb["state"] = "readonly"
        cb.bind("<<ComboboxSelected>>", lambda _e: self.block_selected())
        self.top_frame.rowconfigure(0, weight=0)
        self.top_frame.rowconfigure(1, weight=1)
        self.chars_frame = ScrollableFrame(self.top_frame)
        self.chars_frame.grid(column=0, row=1, sticky="NSEW", padx=5, pady=5)

        self.button_list: list[ttk.Label] = []
        style = ttk.Style()
        style.configure("unicodedialog.TLabel", font=maintext().font)
        self.block_selected()

    def block_selected(self) -> None:
        """Called when a Unicode block is selected."""
        for btn in self.button_list:
            if btn.winfo_exists():
                btn.destroy()
        self.button_list.clear()
        self.chars_frame.reset_scroll()

        def add_button(count: int, char: str) -> None:
            """Add a button to the Unicode block dialog.

            Args:
                count: Count of buttons added, used to determine row/column.
                char: Character to use as label for button.
            """
            btn = ttk.Label(
                self.chars_frame.scrollable_frame,
                text=char,
                width=2,
                borderwidth=2,
                relief=tk.SOLID,
                anchor=tk.CENTER,
                style="unicodedialog.TLabel",
            )

            def press(event: tk.Event) -> None:
                event.widget["relief"] = tk.SUNKEN

            def release(event: tk.Event, char: str) -> None:
                event.widget["relief"] = tk.RAISED
                insert_in_focus_widget(char)

            def enter(event: tk.Event) -> None:
                event.widget["relief"] = tk.RAISED

            def leave(event: tk.Event) -> None:
                relief = str(event.widget["relief"])
                if relief == tk.RAISED:
                    event.widget["relief"] = tk.SOLID

            btn.bind("<ButtonPress-1>", press)
            btn.bind("<ButtonRelease-1>", lambda e: release(e, char))
            btn.bind("<Enter>", enter)
            btn.bind("<Leave>", leave)
            btn.grid(column=count % 16, row=int(count / 16), sticky="NSEW")
            self.button_list.append(btn)

        block_name = re.sub(r" *\(.*", "", preferences.get(PrefKey.UNICODE_BLOCK))
        if block_name == UnicodeBlockDialog.commonly_used_characters_name:
            for count, char in enumerate(_common_characters):
                add_button(count, char)
        else:
            block_range = _unicode_blocks[block_name]
            for count, c_ord in enumerate(range(block_range[0], block_range[1] + 1)):
                add_button(count, chr(c_ord))
        # Add tooltips
        for btn in self.button_list:
            char = str(btn["text"])
            name, new = unicode_char_to_name(char)
            if name:
                name = ": " + name
            warning_flag = "⚠\ufe0f" if new else ""
            ToolTip(btn, f"{warning_flag}U+{ord(char):04x}{name}")


class UnicodeSearchDialog(ToplevelDialog):
    """A dialog that allows user to search for Unicode characters,
    given partial name match or Unicode ordinal, and allows
    the user to insert it into text window."""

    manual_page = "Tools_Menu#Unicode_Search/Entry"
    CHAR_COL_ID = "#1"
    CHAR_COL_HEAD = "Char"
    CHAR_COL_WIDTH = 50
    CODEPOINT_COL_ID = "#2"
    CODEPOINT_COL_HEAD = "Code Point"
    CODEPOINT_COL_WIDTH = 80
    NAME_COL_ID = "#3"
    NAME_COL_HEAD = "Name"
    NAME_COL_WIDTH = 250
    BLOCK_COL_ID = "#4"
    BLOCK_COL_HEAD = "Block"
    BLOCK_COL_WIDTH = 180

    def __init__(self) -> None:
        """Initialize Unicode Search dialog."""

        super().__init__("Unicode Search")

        search_frame = ttk.Frame(self.top_frame)
        search_frame.grid(column=0, row=0, sticky="NSEW")
        search_frame.columnconfigure(0, weight=1)
        search_frame.columnconfigure(1, weight=0)
        self.search = Combobox(
            search_frame,
            PrefKey.UNICODE_SEARCH_HISTORY,
            width=50,
            font=maintext().font,
        )
        self.search.grid(column=0, row=0, sticky="NSEW", padx=5, pady=(5, 0))
        self.search.focus()
        ToolTip(
            self.search,
            "\n".join(
                [
                    "Type words from character name to match against,",
                    "or hex codepoint (optionally preceded by 'U+' or 'X'),",
                    "or type/paste a single character to search for",
                ]
            ),
        )

        search_btn = ttk.Button(
            search_frame,
            text="Search",
            default="active",
            takefocus=False,
            command=lambda: self.find_matches(self.search.get()),
        )
        search_btn.grid(column=1, row=0, sticky="NSEW")
        self.bind("<Return>", lambda _: search_btn.invoke())
        self.search.bind("<<ComboboxSelected>>", lambda _e: search_btn.invoke())

        self.top_frame.rowconfigure(0, weight=0)
        self.top_frame.rowconfigure(1, weight=1)

        columns = (
            UnicodeSearchDialog.CHAR_COL_HEAD,
            UnicodeSearchDialog.CODEPOINT_COL_HEAD,
            UnicodeSearchDialog.NAME_COL_HEAD,
            UnicodeSearchDialog.BLOCK_COL_HEAD,
        )
        widths = (
            UnicodeSearchDialog.CHAR_COL_WIDTH,
            UnicodeSearchDialog.CODEPOINT_COL_WIDTH,
            UnicodeSearchDialog.NAME_COL_WIDTH,
            UnicodeSearchDialog.BLOCK_COL_WIDTH,
        )
        self.list = ttk.Treeview(
            self.top_frame,
            columns=columns,
            show="headings",
            height=15,
            selectmode=tk.BROWSE,
        )
        ToolTip(
            self.list,
            "\n".join(
                [
                    f"Click in {UnicodeSearchDialog.CHAR_COL_HEAD},  {UnicodeSearchDialog.CODEPOINT_COL_HEAD} or {UnicodeSearchDialog.NAME_COL_HEAD} column to insert character",
                    f"Click in {UnicodeSearchDialog.BLOCK_COL_HEAD} column to open Unicode Block dialog",
                    "(⚠\ufe0f before a character's name means it was added more recently - use with caution)",
                ]
            ),
            use_pointer_pos=True,
        )
        for col, column in enumerate(columns):
            col_id = f"#{col + 1}"
            anchor = tk.CENTER if col_id == UnicodeSearchDialog.CHAR_COL_ID else tk.W
            self.list.column(  # type: ignore[call-overload]
                col_id,
                minwidth=20,
                width=widths[col],
                stretch=(col_id == UnicodeSearchDialog.NAME_COL_ID),
                anchor=anchor,
            )
            self.list.heading(col_id, text=column, anchor=anchor)  # type: ignore[call-overload]
        self.list.grid(row=1, column=0, sticky=tk.NSEW, pady=(5, 0))

        self.scrollbar = ttk.Scrollbar(
            self.top_frame, orient=tk.VERTICAL, command=self.list.yview
        )
        self.list.configure(yscroll=self.scrollbar.set)  # type: ignore[call-overload]
        self.scrollbar.grid(row=1, column=1, sticky=tk.NS)
        mouse_bind(self.list, "1", self.item_clicked)

    def find_matches(self, string: str) -> None:
        """Find & display Unicode characters that match all criteria (words) in the given string.

        Args:
            string: String containing words that must appear in the characters' names. If no match
                    is found, check if string is a hex number which is the codepoint of a character.
        """
        # Clear existing display of chars
        children = self.list.get_children()
        for child in children:
            self.list.delete(child)

        # Split user string into words
        match_words = [x.lower() for x in string.split(" ") if x]
        if len(match_words) > 0:
            self.search.add_to_history(string)

        # Check every Unicode character to see if its name contains all the given words
        # (including hyphenated, e.g. BREAK will match NO-BREAK, but not NON-BREAKING)
        found = False
        if len(match_words) > 0:
            for ordinal in range(0, sys.maxunicode + 1):
                char = chr(ordinal)
                name, new = unicode_char_to_name(char)
                name_list = name.lower().split(" ")
                hyphen_parts: list[str] = []
                for word in name_list:
                    if "-" in word:
                        hyphen_parts += word.split("-")
                name_list += hyphen_parts
                if name and all(word in name_list for word in match_words):
                    self.add_row(char, new)
                    found = True

        if not found:  # Maybe string was a hex codepoint?
            hex_string = re.sub(r"^(U\+|0?X)", "", string.strip(), flags=re.IGNORECASE)
            char = ""
            try:
                char = chr(int(hex_string, 16))
            except (OverflowError, ValueError):
                pass
            if char:
                name, new = unicode_char_to_name(char)
                if name:
                    self.add_row(char, new)
                    found = True

        # Maybe string was a single character?
        # Carefully strip spaces, so that a single space still works
        if not found and len(string) >= 1:
            string = string.strip(" ")
            if len(string) == 0:
                string = " "  # String was all spaces
            if len(string) == 1:
                name, new = unicode_char_to_name(string)
                if name:
                    self.add_row(string, new)
                    found = True

        if not found:
            sound_bell()

    def add_row(self, char: str, new: bool) -> None:
        """Add a row to the Unicode Search dialog.

        Args:
            count: Row to add.
            char: Character to display in row.
            new: True if character was added to unicode since version 3.2 (March 2002)
        """
        ordinal = ord(char)
        block_name = ""
        warning_flag = "⚠\ufe0f" if new else ""
        # Find block name
        for block, (beg, end, _) in _unicode_blocks.items():
            if beg <= ordinal <= end:
                block_name = block
                break
        # Add entry to Treeview
        entry = (
            char,
            f"U+{ord(char):04x}",
            warning_flag + unicodedata.name(char),
            block_name,
        )
        self.list.insert("", tk.END, values=entry)

    def item_clicked(self, event: tk.Event) -> None:
        """Called when Unicode search item is clicked.

        If click is in char, codepoint or name column, then insert character.
        If in block column, open the block dialog.

        Args:
            event: Event containing location of mouse click
        """
        row_id = self.list.identify_row(event.y)
        row = self.list.set(row_id)
        if not row:  # Heading clicked
            return
        col_id = self.list.identify_column(event.x)

        if col_id in (
            UnicodeSearchDialog.CHAR_COL_ID,
            UnicodeSearchDialog.CODEPOINT_COL_ID,
            UnicodeSearchDialog.NAME_COL_ID,
        ):
            insert_in_focus_widget(row[UnicodeSearchDialog.CHAR_COL_HEAD])
        elif col_id == UnicodeSearchDialog.BLOCK_COL_ID:
            block = row[UnicodeSearchDialog.BLOCK_COL_HEAD]
            if not block:
                return
            preferences.set(PrefKey.UNICODE_BLOCK, block)
            dlg = UnicodeBlockDialog.show_dialog()
            dlg.block_selected()


def unicode_char_to_name(char: str) -> tuple[str, bool]:
    """Convert char to Unicode name, and note if it is "new".

    Args:
        char: Character to convert.

    Returns:
        Tuple containing name of character (empty if no name),
        and bool flagging if character is new (Unicode version > 3.2).
    """
    new = False
    try:
        name = unicodedata.name(char)
        try:
            unicodedata.ucd_3_2_0.name(char)
        except ValueError:
            new = True
    except ValueError:
        name = ""
    return name, new


# Somewhat arbitrarily, certain Unicode blocks are not displayed in the
# dropdown menu, roughly matching the GG1 list, and trying to
# balance usefulness with excessive number of blocks to choose from.
# List of blocks taken from https://unicode.org/Public/UNIDATA/Blocks.txt with header
#     Blocks-16.0.0.txt
#     Date: 2024-02-02
# Start point of Basic Latin and Latin-1 Supplement adjusted to avoid unprintables
_unicode_blocks: dict[str, tuple[int, int, bool]] = {
    "Basic Latin": (0x0020, 0x007E, True),
    "Latin-1 Supplement": (0x00A0, 0x00FF, True),
    "Latin Extended-A": (0x0100, 0x017F, True),
    "Latin Extended-B": (0x0180, 0x024F, True),
    "IPA Extensions": (0x0250, 0x02AF, True),
    "Spacing Modifier Letters": (0x02B0, 0x02FF, True),
    "Combining Diacritical Marks": (0x0300, 0x036F, True),
    "Greek and Coptic": (0x0370, 0x03FF, True),
    "Cyrillic": (0x0400, 0x04FF, True),
    "Cyrillic Supplement": (0x0500, 0x052F, True),
    "Armenian": (0x0530, 0x058F, True),
    "Hebrew": (0x0590, 0x05FF, True),
    "Arabic": (0x0600, 0x06FF, True),
    "Syriac": (0x0700, 0x074F, True),
    "Arabic Supplement": (0x0750, 0x077F, False),
    "Thaana": (0x0780, 0x07BF, True),
    "NKo": (0x07C0, 0x07FF, False),
    "Samaritan": (0x0800, 0x083F, False),
    "Mandaic": (0x0840, 0x085F, False),
    "Syriac Supplement": (0x0860, 0x086F, False),
    "Arabic Extended-B": (0x0870, 0x089F, False),
    "Arabic Extended-A": (0x08A0, 0x08FF, False),
    "Devanagari": (0x0900, 0x097F, True),
    "Bengali": (0x0980, 0x09FF, True),
    "Gurmukhi": (0x0A00, 0x0A7F, True),
    "Gujarati": (0x0A80, 0x0AFF, True),
    "Oriya": (0x0B00, 0x0B7F, True),
    "Tamil": (0x0B80, 0x0BFF, True),
    "Telugu": (0x0C00, 0x0C7F, True),
    "Kannada": (0x0C80, 0x0CFF, True),
    "Malayalam": (0x0D00, 0x0D7F, True),
    "Sinhala": (0x0D80, 0x0DFF, True),
    "Thai": (0x0E00, 0x0E7F, True),
    "Lao": (0x0E80, 0x0EFF, True),
    "Tibetan": (0x0F00, 0x0FFF, False),
    "Myanmar": (0x1000, 0x109F, True),
    "Georgian": (0x10A0, 0x10FF, True),
    "Hangul Jamo": (0x1100, 0x11FF, True),
    "Ethiopic": (0x1200, 0x137F, True),
    "Ethiopic Supplement": (0x1380, 0x139F, False),
    "Cherokee": (0x13A0, 0x13FF, True),
    "Unified Canadian Aboriginal Syllabics": (0x1400, 0x167F, True),
    "Ogham": (0x1680, 0x169F, True),
    "Runic": (0x16A0, 0x16FF, True),
    "Tagalog": (0x1700, 0x171F, True),
    "Hanunoo": (0x1720, 0x173F, False),
    "Buhid": (0x1740, 0x175F, True),
    "Tagbanwa": (0x1760, 0x177F, False),
    "Khmer": (0x1780, 0x17FF, False),
    "Mongolian": (0x1800, 0x18AF, True),
    "Unified Canadian Aboriginal Syllabics Extended": (0x18B0, 0x18FF, False),
    "Limbu": (0x1900, 0x194F, False),
    "Tai Le": (0x1950, 0x197F, False),
    "New Tai Lue": (0x1980, 0x19DF, False),
    "Khmer Symbols": (0x19E0, 0x19FF, False),
    "Buginese": (0x1A00, 0x1A1F, False),
    "Tai Tham": (0x1A20, 0x1AAF, False),
    "Combining Diacritical Marks Extended": (0x1AB0, 0x1AFF, False),
    "Balinese": (0x1B00, 0x1B7F, False),
    "Sundanese": (0x1B80, 0x1BBF, False),
    "Batak": (0x1BC0, 0x1BFF, False),
    "Lepcha": (0x1C00, 0x1C4F, False),
    "Ol Chiki": (0x1C50, 0x1C7F, False),
    "Cyrillic Extended-C": (0x1C80, 0x1C8F, False),
    "Georgian Extended": (0x1C90, 0x1CBF, False),
    "Sundanese Supplement": (0x1CC0, 0x1CCF, False),
    "Vedic Extensions": (0x1CD0, 0x1CFF, False),
    "Phonetic Extensions": (0x1D00, 0x1D7F, True),
    "Phonetic Extensions Supplement": (0x1D80, 0x1DBF, False),
    "Combining Diacritical Marks Supplement": (0x1DC0, 0x1DFF, False),
    "Latin Extended Additional": (0x1E00, 0x1EFF, True),
    "Greek Extended": (0x1F00, 0x1FFF, True),
    "General Punctuation": (0x2000, 0x206F, True),
    "Superscripts and Subscripts": (0x2070, 0x209F, True),
    "Currency Symbols": (0x20A0, 0x20CF, True),
    "Combining Diacritical Marks for Symbols": (0x20D0, 0x20FF, True),
    "Letterlike Symbols": (0x2100, 0x214F, True),
    "Number Forms": (0x2150, 0x218F, True),
    "Arrows": (0x2190, 0x21FF, True),
    "Mathematical Operators": (0x2200, 0x22FF, True),
    "Miscellaneous Technical": (0x2300, 0x23FF, True),
    "Control Pictures": (0x2400, 0x243F, True),
    "Optical Character Recognition": (0x2440, 0x245F, True),
    "Enclosed Alphanumerics": (0x2460, 0x24FF, True),
    "Box Drawing": (0x2500, 0x257F, True),
    "Block Elements": (0x2580, 0x259F, True),
    "Geometric Shapes": (0x25A0, 0x25FF, True),
    "Miscellaneous Symbols": (0x2600, 0x26FF, True),
    "Dingbats": (0x2700, 0x27BF, True),
    "Miscellaneous Mathematical Symbols-A": (0x27C0, 0x27EF, True),
    "Supplemental Arrows-A": (0x27F0, 0x27FF, True),
    "Braille Patterns": (0x2800, 0x28FF, True),
    "Supplemental Arrows-B": (0x2900, 0x297F, True),
    "Miscellaneous Mathematical Symbols-B": (0x2980, 0x29FF, True),
    "Supplemental Mathematical Operators": (0x2A00, 0x2AFF, True),
    "Miscellaneous Symbols and Arrows": (0x2B00, 0x2BFF, True),
    "Glagolitic": (0x2C00, 0x2C5F, False),
    "Latin Extended-C": (0x2C60, 0x2C7F, False),
    "Coptic": (0x2C80, 0x2CFF, False),
    "Georgian Supplement": (0x2D00, 0x2D2F, False),
    "Tifinagh": (0x2D30, 0x2D7F, False),
    "Ethiopic Extended": (0x2D80, 0x2DDF, False),
    "Cyrillic Extended-A": (0x2DE0, 0x2DFF, False),
    "Supplemental Punctuation": (0x2E00, 0x2E7F, False),
    "CJK Radicals Supplement": (0x2E80, 0x2EFF, False),
    "Kangxi Radicals": (0x2F00, 0x2FDF, False),
    "Ideographic Description Characters": (0x2FF0, 0x2FFF, False),
    "CJK Symbols and Punctuation": (0x3000, 0x303F, False),
    "Hiragana": (0x3040, 0x309F, False),
    "Katakana": (0x30A0, 0x30FF, False),
    "Bopomofo": (0x3100, 0x312F, False),
    "Hangul Compatibility Jamo": (0x3130, 0x318F, False),
    "Kanbun": (0x3190, 0x319F, False),
    "Bopomofo Extended": (0x31A0, 0x31BF, False),
    "CJK Strokes": (0x31C0, 0x31EF, False),
    "Katakana Phonetic Extensions": (0x31F0, 0x31FF, False),
    "Enclosed CJK Letters and Months": (0x3200, 0x32FF, False),
    "CJK Compatibility": (0x3300, 0x33FF, False),
    "CJK Unified Ideographs Extension A": (0x3400, 0x4DBF, False),
    "Yijing Hexagram Symbols": (0x4DC0, 0x4DFF, False),
    "CJK Unified Ideographs": (0x4E00, 0x9FFF, False),
    "Yi Syllables": (0xA000, 0xA48F, False),
    "Yi Radicals": (0xA490, 0xA4CF, False),
    "Lisu": (0xA4D0, 0xA4FF, False),
    "Vai": (0xA500, 0xA63F, False),
    "Cyrillic Extended-B": (0xA640, 0xA69F, False),
    "Bamum": (0xA6A0, 0xA6FF, False),
    "Modifier Tone Letters": (0xA700, 0xA71F, False),
    "Latin Extended-D": (0xA720, 0xA7FF, False),
    "Syloti Nagri": (0xA800, 0xA82F, False),
    "Common Indic Number Forms": (0xA830, 0xA83F, False),
    "Phags-pa": (0xA840, 0xA87F, False),
    "Saurashtra": (0xA880, 0xA8DF, False),
    "Devanagari Extended": (0xA8E0, 0xA8FF, False),
    "Kayah Li": (0xA900, 0xA92F, False),
    "Rejang": (0xA930, 0xA95F, False),
    "Hangul Jamo Extended-A": (0xA960, 0xA97F, False),
    "Javanese": (0xA980, 0xA9DF, False),
    "Myanmar Extended-B": (0xA9E0, 0xA9FF, False),
    "Cham": (0xAA00, 0xAA5F, False),
    "Myanmar Extended-A": (0xAA60, 0xAA7F, False),
    "Tai Viet": (0xAA80, 0xAADF, False),
    "Meetei Mayek Extensions": (0xAAE0, 0xAAFF, False),
    "Ethiopic Extended-A": (0xAB00, 0xAB2F, False),
    "Latin Extended-E": (0xAB30, 0xAB6F, False),
    "Cherokee Supplement": (0xAB70, 0xABBF, False),
    "Meetei Mayek": (0xABC0, 0xABFF, False),
    "Hangul Syllables": (0xAC00, 0xD7AF, False),
    "Hangul Jamo Extended-B": (0xD7B0, 0xD7FF, False),
    "High Surrogates": (0xD800, 0xDB7F, False),
    "High Private Use Surrogates": (0xDB80, 0xDBFF, False),
    "Low Surrogates": (0xDC00, 0xDFFF, False),
    "Private Use Area": (0xE000, 0xF8FF, False),
    "CJK Compatibility Ideographs": (0xF900, 0xFAFF, False),
    "Alphabetic Presentation Forms": (0xFB00, 0xFB4F, True),
    "Arabic Presentation Forms-A": (0xFB50, 0xFDFF, True),
    "Variation Selectors": (0xFE00, 0xFE0F, True),
    "Vertical Forms": (0xFE10, 0xFE1F, False),
    "Combining Half Marks": (0xFE20, 0xFE2F, True),
    "CJK Compatibility Forms": (0xFE30, 0xFE4F, False),
    "Small Form Variants": (0xFE50, 0xFE6F, True),
    "Arabic Presentation Forms-B": (0xFE70, 0xFEFF, True),
    "Halfwidth and Fullwidth Forms": (0xFF00, 0xFFEF, True),
    "Specials": (0xFFF0, 0xFFFF, False),
    "Linear B Syllabary": (0x10000, 0x1007F, False),
    "Linear B Ideograms": (0x10080, 0x100FF, False),
    "Aegean Numbers": (0x10100, 0x1013F, False),
    "Ancient Greek Numbers": (0x10140, 0x1018F, False),
    "Ancient Symbols": (0x10190, 0x101CF, False),
    "Phaistos Disc": (0x101D0, 0x101FF, False),
    "Lycian": (0x10280, 0x1029F, False),
    "Carian": (0x102A0, 0x102DF, False),
    "Coptic Epact Numbers": (0x102E0, 0x102FF, False),
    "Old Italic": (0x10300, 0x1032F, False),
    "Gothic": (0x10330, 0x1034F, False),
    "Old Permic": (0x10350, 0x1037F, False),
    "Ugaritic": (0x10380, 0x1039F, False),
    "Old Persian": (0x103A0, 0x103DF, False),
    "Deseret": (0x10400, 0x1044F, False),
    "Shavian": (0x10450, 0x1047F, False),
    "Osmanya": (0x10480, 0x104AF, False),
    "Osage": (0x104B0, 0x104FF, False),
    "Elbasan": (0x10500, 0x1052F, False),
    "Caucasian Albanian": (0x10530, 0x1056F, False),
    "Vithkuqi": (0x10570, 0x105BF, False),
    "Todhri": (0x105C0, 0x105FF, False),
    "Linear A": (0x10600, 0x1077F, False),
    "Latin Extended-F": (0x10780, 0x107BF, False),
    "Cypriot Syllabary": (0x10800, 0x1083F, False),
    "Imperial Aramaic": (0x10840, 0x1085F, False),
    "Palmyrene": (0x10860, 0x1087F, False),
    "Nabataean": (0x10880, 0x108AF, False),
    "Hatran": (0x108E0, 0x108FF, False),
    "Phoenician": (0x10900, 0x1091F, False),
    "Lydian": (0x10920, 0x1093F, False),
    "Meroitic Hieroglyphs": (0x10980, 0x1099F, False),
    "Meroitic Cursive": (0x109A0, 0x109FF, False),
    "Kharoshthi": (0x10A00, 0x10A5F, False),
    "Old South Arabian": (0x10A60, 0x10A7F, False),
    "Old North Arabian": (0x10A80, 0x10A9F, False),
    "Manichaean": (0x10AC0, 0x10AFF, False),
    "Avestan": (0x10B00, 0x10B3F, False),
    "Inscriptional Parthian": (0x10B40, 0x10B5F, False),
    "Inscriptional Pahlavi": (0x10B60, 0x10B7F, False),
    "Psalter Pahlavi": (0x10B80, 0x10BAF, False),
    "Old Turkic": (0x10C00, 0x10C4F, False),
    "Old Hungarian": (0x10C80, 0x10CFF, False),
    "Hanifi Rohingya": (0x10D00, 0x10D3F, False),
    "Garay": (0x10D40, 0x10D8F, False),
    "Rumi Numeral Symbols": (0x10E60, 0x10E7F, False),
    "Yezidi": (0x10E80, 0x10EBF, False),
    "Arabic Extended-C": (0x10EC0, 0x10EFF, False),
    "Old Sogdian": (0x10F00, 0x10F2F, False),
    "Sogdian": (0x10F30, 0x10F6F, False),
    "Old Uyghur": (0x10F70, 0x10FAF, False),
    "Chorasmian": (0x10FB0, 0x10FDF, False),
    "Elymaic": (0x10FE0, 0x10FFF, False),
    "Brahmi": (0x11000, 0x1107F, False),
    "Kaithi": (0x11080, 0x110CF, False),
    "Sora Sompeng": (0x110D0, 0x110FF, False),
    "Chakma": (0x11100, 0x1114F, False),
    "Mahajani": (0x11150, 0x1117F, False),
    "Sharada": (0x11180, 0x111DF, False),
    "Sinhala Archaic Numbers": (0x111E0, 0x111FF, False),
    "Khojki": (0x11200, 0x1124F, False),
    "Multani": (0x11280, 0x112AF, False),
    "Khudawadi": (0x112B0, 0x112FF, False),
    "Grantha": (0x11300, 0x1137F, False),
    "Tulu-Tigalari": (0x11380, 0x113FF, False),
    "Newa": (0x11400, 0x1147F, False),
    "Tirhuta": (0x11480, 0x114DF, False),
    "Siddham": (0x11580, 0x115FF, False),
    "Modi": (0x11600, 0x1165F, False),
    "Mongolian Supplement": (0x11660, 0x1167F, False),
    "Takri": (0x11680, 0x116CF, False),
    "Myanmar Extended-C": (0x116D0, 0x116FF, False),
    "Ahom": (0x11700, 0x1174F, False),
    "Dogra": (0x11800, 0x1184F, False),
    "Warang Citi": (0x118A0, 0x118FF, False),
    "Dives Akuru": (0x11900, 0x1195F, False),
    "Nandinagari": (0x119A0, 0x119FF, False),
    "Zanabazar Square": (0x11A00, 0x11A4F, False),
    "Soyombo": (0x11A50, 0x11AAF, False),
    "Unified Canadian Aboriginal Syllabics Extended-A": (0x11AB0, 0x11ABF, False),
    "Pau Cin Hau": (0x11AC0, 0x11AFF, False),
    "Devanagari Extended-A": (0x11B00, 0x11B5F, False),
    "Sunuwar": (0x11BC0, 0x11BFF, False),
    "Bhaiksuki": (0x11C00, 0x11C6F, False),
    "Marchen": (0x11C70, 0x11CBF, False),
    "Masaram Gondi": (0x11D00, 0x11D5F, False),
    "Gunjala Gondi": (0x11D60, 0x11DAF, False),
    "Makasar": (0x11EE0, 0x11EFF, False),
    "Kawi": (0x11F00, 0x11F5F, False),
    "Lisu Supplement": (0x11FB0, 0x11FBF, False),
    "Tamil Supplement": (0x11FC0, 0x11FFF, False),
    "Cuneiform": (0x12000, 0x123FF, False),
    "Cuneiform Numbers and Punctuation": (0x12400, 0x1247F, False),
    "Early Dynastic Cuneiform": (0x12480, 0x1254F, False),
    "Cypro-Minoan": (0x12F90, 0x12FFF, False),
    "Egyptian Hieroglyphs": (0x13000, 0x1342F, False),
    "Egyptian Hieroglyph Format Controls": (0x13430, 0x1345F, False),
    "Egyptian Hieroglyphs Extended-A": (0x13460, 0x143FF, False),
    "Anatolian Hieroglyphs": (0x14400, 0x1467F, False),
    "Gurung Khema": (0x16100, 0x1613F, False),
    "Bamum Supplement": (0x16800, 0x16A3F, False),
    "Mro": (0x16A40, 0x16A6F, False),
    "Tangsa": (0x16A70, 0x16ACF, False),
    "Bassa Vah": (0x16AD0, 0x16AFF, False),
    "Pahawh Hmong": (0x16B00, 0x16B8F, False),
    "Kirat Rai": (0x16D40, 0x16D7F, False),
    "Medefaidrin": (0x16E40, 0x16E9F, False),
    "Miao": (0x16F00, 0x16F9F, False),
    "Ideographic Symbols and Punctuation": (0x16FE0, 0x16FFF, False),
    "Tangut": (0x17000, 0x187FF, False),
    "Tangut Components": (0x18800, 0x18AFF, False),
    "Khitan Small Script": (0x18B00, 0x18CFF, False),
    "Tangut Supplement": (0x18D00, 0x18D7F, False),
    "Kana Extended-B": (0x1AFF0, 0x1AFFF, False),
    "Kana Supplement": (0x1B000, 0x1B0FF, False),
    "Kana Extended-A": (0x1B100, 0x1B12F, False),
    "Small Kana Extension": (0x1B130, 0x1B16F, False),
    "Nushu": (0x1B170, 0x1B2FF, False),
    "Duployan": (0x1BC00, 0x1BC9F, False),
    "Shorthand Format Controls": (0x1BCA0, 0x1BCAF, False),
    "Symbols for Legacy Computing Supplement": (0x1CC00, 0x1CEBF, False),
    "Znamenny Musical Notation": (0x1CF00, 0x1CFCF, False),
    "Byzantine Musical Symbols": (0x1D000, 0x1D0FF, False),
    "Musical Symbols": (0x1D100, 0x1D1FF, False),
    "Ancient Greek Musical Notation": (0x1D200, 0x1D24F, False),
    "Kaktovik Numerals": (0x1D2C0, 0x1D2DF, False),
    "Mayan Numerals": (0x1D2E0, 0x1D2FF, False),
    "Tai Xuan Jing Symbols": (0x1D300, 0x1D35F, False),
    "Counting Rod Numerals": (0x1D360, 0x1D37F, False),
    "Mathematical Alphanumeric Symbols": (0x1D400, 0x1D7FF, False),
    "Sutton SignWriting": (0x1D800, 0x1DAAF, False),
    "Latin Extended-G": (0x1DF00, 0x1DFFF, False),
    "Glagolitic Supplement": (0x1E000, 0x1E02F, False),
    "Cyrillic Extended-D": (0x1E030, 0x1E08F, False),
    "Nyiakeng Puachue Hmong": (0x1E100, 0x1E14F, False),
    "Toto": (0x1E290, 0x1E2BF, False),
    "Wancho": (0x1E2C0, 0x1E2FF, False),
    "Nag Mundari": (0x1E4D0, 0x1E4FF, False),
    "Ol Onal": (0x1E5D0, 0x1E5FF, False),
    "Ethiopic Extended-B": (0x1E7E0, 0x1E7FF, False),
    "Mende Kikakui": (0x1E800, 0x1E8DF, False),
    "Adlam": (0x1E900, 0x1E95F, False),
    "Indic Siyaq Numbers": (0x1EC70, 0x1ECBF, False),
    "Ottoman Siyaq Numbers": (0x1ED00, 0x1ED4F, False),
    "Arabic Mathematical Alphabetic Symbols": (0x1EE00, 0x1EEFF, False),
    "Mahjong Tiles": (0x1F000, 0x1F02F, False),
    "Domino Tiles": (0x1F030, 0x1F09F, False),
    "Playing Cards": (0x1F0A0, 0x1F0FF, False),
    "Enclosed Alphanumeric Supplement": (0x1F100, 0x1F1FF, False),
    "Enclosed Ideographic Supplement": (0x1F200, 0x1F2FF, False),
    "Miscellaneous Symbols and Pictographs": (0x1F300, 0x1F5FF, False),
    "Emoticons": (0x1F600, 0x1F64F, False),
    "Ornamental Dingbats": (0x1F650, 0x1F67F, False),
    "Transport and Map Symbols": (0x1F680, 0x1F6FF, False),
    "Alchemical Symbols": (0x1F700, 0x1F77F, False),
    "Geometric Shapes Extended": (0x1F780, 0x1F7FF, False),
    "Supplemental Arrows-C": (0x1F800, 0x1F8FF, False),
    "Supplemental Symbols and Pictographs": (0x1F900, 0x1F9FF, False),
    "Chess Symbols": (0x1FA00, 0x1FA6F, False),
    "Symbols and Pictographs Extended-A": (0x1FA70, 0x1FAFF, False),
    "Symbols for Legacy Computing": (0x1FB00, 0x1FBFF, False),
    "CJK Unified Ideographs Extension B": (0x20000, 0x2A6DF, False),
    "CJK Unified Ideographs Extension C": (0x2A700, 0x2B73F, False),
    "CJK Unified Ideographs Extension D": (0x2B740, 0x2B81F, False),
    "CJK Unified Ideographs Extension E": (0x2B820, 0x2CEAF, False),
    "CJK Unified Ideographs Extension F": (0x2CEB0, 0x2EBEF, False),
    "CJK Unified Ideographs Extension I": (0x2EBF0, 0x2EE5F, False),
    "CJK Compatibility Ideographs Supplement": (0x2F800, 0x2FA1F, False),
    "CJK Unified Ideographs Extension G": (0x30000, 0x3134F, False),
    "CJK Unified Ideographs Extension H": (0x31350, 0x323AF, False),
    "Tags": (0xE0000, 0xE007F, False),
    "Variation Selectors Supplement": (0xE0100, 0xE01EF, False),
    "Supplementary Private Use Area-A": (0xF0000, 0xFFFFF, False),
    "Supplementary Private Use Area-B": (0x100000, 0x10FFFF, False),
}


_common_characters: list[str] = [
    "À",
    "Á",
    "Â",
    "Ã",
    "Ä",
    "Å",
    "Æ",
    "Ç",
    "È",
    "É",
    "Ê",
    "Ë",
    "Ì",
    "Í",
    "Î",
    "Ï",
    "Ò",
    "Ó",
    "Ô",
    "Õ",
    "Ö",
    "Ø",
    "Œ",
    "Ñ",
    "Ù",
    "Ú",
    "Û",
    "Ü",
    "Ð",
    "þ",
    "Ÿ",
    "Ý",
    "à",
    "á",
    "â",
    "ã",
    "ä",
    "å",
    "æ",
    "ç",
    "è",
    "é",
    "ê",
    "ë",
    "ì",
    "í",
    "î",
    "ï",
    "ò",
    "ó",
    "ô",
    "õ",
    "ö",
    "ø",
    "œ",
    "ñ",
    "ù",
    "ú",
    "û",
    "ü",
    "ð",
    "Þ",
    "ÿ",
    "ý",
    "¡",
    "¿",
    "«",
    "»",
    "‘",
    "’",
    "“",
    "”",
    "‚",
    "‛",
    "„",
    "‟",
    "ß",
    "⁂",
    "☞",
    "☜",
    "±",
    "·",
    "×",
    "÷",
    "°",
    "′",
    "″",
    "‴",
    "‰",
    "¹",
    "²",
    "³",
    "£",
    "¢",
    "©",
    "\xa0",
    "½",
    "⅓",
    "⅔",
    "¼",
    "¾",
    "⅕",
    "⅖",
    "⅗",
    "⅘",
    "⅙",
    "⅚",
    "⅐",
    "⅛",
    "⅜",
    "⅝",
    "⅞",
    "—",
    "–",
    "†",
    "‡",
    "§",
    "‖",
    "¶",
    "¦",
    "º",
    "ª",
]

_unicode_names: dict[str, list[str]] = {}
