"""Functions for making and managing notation cards"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb.

# %% auto 0
__all__ = ['MAX_NOTE_NAME_LENGTH', 'SPECIAL_CHARACTERS', 'replaceable_groups', 'REPLACEABLES', 'notations_to_add_in_index',
           'index_notation_note_formatted_entry', 'make_a_notation_note', 'make_notation_notes_from_double_asts',
           'make_notation_notes_from_HTML_tags', 'notation_note_has_no_verified_content', 'remove_bad_notation_notes',
           'reorder_notation_note_links_in_see_also_section', 'regex_from_latex', 'regex_from_notation_note']

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 2
from os import PathLike
from pathlib import Path
from typing import Optional, Union
import warnings

from multiset import Multiset
from pylatexenc.latexwalker import LatexNode, LatexMacroNode, LatexWalker, LatexGroupNode, LatexCharsNode

from .....helper.html import remove_html_tags_in_text
from .....helper.path_accepted_string import latex_to_path_accepted_string
from ....markdown.file import MarkdownFile, MarkdownLineEnum
from trouver.markdown.obsidian.links import (
    LinkType, LinkFormatError, ObsidianLink
)
from .parse import notation_in_note, parse_notation_note
from trouver.markdown.obsidian.personal.notation.in_standard_information_note import (
    notat_str_from_doub_asts_in_std_info_note, notations_and_main_notes,
    add_notation_note_to_see_also, notation_note_is_linked_in_see_also_section,
    notat_str_from_html_tags
)
from trouver.markdown.obsidian.personal.note_type import (
    PersonalNoteTypeEnum, note_is_of_type
)
from ...vault import VaultNote
from ...vault_and_links import all_links_in_vault
# import trouver.markdown.obsidian.vault_and_links

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 5
def notations_to_add_in_index(
        vault: PathLike, # Path to the vault directory.
        notation_index_note = VaultNote, # The notation index note in the vault where the notations should be added to.
        subdirectory: Optional[PathLike] = None , # Path to the subdirectory, relative to `vault`, to find the notation notes. Searches for all notation notes here and in subdirectories of this subdirectory. If `None`, then the `note parameter is used to determined the subdirectory. If `subdirectory` is the empty str, then all notation notes in the vault are searched. Defaults to `None`. 
        note: Optional[VaultNote] = None # The directory that this note is in determines the argument to `subdirectory` parameter if it is `None`. Defaults to `None`, in which case `subdirectory` must be specified.
        ) -> list[tuple[str, ObsidianLink]]: # Each tuple in the list consists of the notation str of the notation note (including surrounding dollar signs `$`) and the (nonembedded) ObsidianLink object for a link to the notation note.
    """Returns notations and links of notation notes to that ought to be
    added in the corresponding notation index, i.e. are in the reference
    folder but not linked by the notation index note.

    If a notation note is not properly formatted, e.g. does not have a
    notation, then the notation and link for the notation note will not
    be included.
    
    **Raises**
    - ValueError
        - If `subdirectory` and `note` are both `None`.

    """
    vault = vault if vault is not None else ''
    mains_dict = notations_and_main_notes(vault, subdirectory, note)
    mf_object = MarkdownFile.from_file(notation_index_note.path())
    mf_text = str(mf_object)
    notations_and_links = []
    for notation, _ in mains_dict.items():
        link_object = ObsidianLink(
            is_embedded=True, file_name=notation, anchor=0, custom_text=0,
            link_type=LinkType.WIKILINK)
        link = link_object.to_string()
        try:
            notation_str = notation_in_note(notation, vault)
        except AttributeError:  # When a notation note is incomplete.
            continue  # TODO: print a warning
        if not link in mf_text:
            notations_and_links.append((notation_str, link_object))
    return notations_and_links



# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 8
def index_notation_note_formatted_entry(
        notation_str: str, # The str of the notation, including the surrounding dollar signs `$`.
        link: ObsidianLink # The embedded link to the notation note. 
        ) -> str:
    """Return a str formatted for an index notation note entry.

    It is recommended to pass the outputs of
    `notations_to_add_in_index` to this function.
    """
    return f'### {notation_str}\n- {link.to_string()}'

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 12
def make_a_notation_note(
        main_note: VaultNote, # The note from which the notation originates.
        vault: PathLike,
        notation: str, # The notation typed in latex. May or may not be surrounded by dollar signs
        description: str, # The rest of the text describing notation.
        notation_note_name: str, # The name of the new notation note to be created.
        destination: Optional[PathLike] = None, # The directory to create the new notation note in.  If `None`, then creates the new notation note in the same place as the note specified by `note_name`
        overwrite: bool = False, # If `True`, overwrite file of the same path as the new notation file to be written, if such a file exists.  Otherwise, does nothing. Even if a link to the old notation note exists in `main_note`, a new link will still be added.  Defaults to `False`.
        add_to_main: bool = True, # If `True`, adds a link to the notation note in the `See Also` section of the main note.
        latex_in_original: str = '' # The full math mode string in `main_note` which introduces the notation. Defaults to the blank string `''`, in which case `notation` plays the role of `latex_in_original`
        ) -> Union[VaultNote, None]: # The newly created notation note. If no note is created, then returns `None`.
    """Make a new notation note, optionally add a link to it in the
    `See Also` section of its main note, returns it.

    The notation note is created in the same directory as the main note.
    The meta of the notation note has a `latex_in_original` section which
    lists the contents of the latex string in the main note from which the
    notation note comes from. This is so that the
    `make_notation_notes_from_double_asts` method can distinguish between
    notations for which a note has been created and for which a note has
    not been created.
    """
    if destination is None:
        destination = main_note.directory(relative=True)
    notation_note = VaultNote(
        vault, rel_path=destination / f'{notation_note_name}.md')
    if not overwrite and notation_note.exists():
        return
    if not notation_note.exists():
        notation_note.create()
    if not latex_in_original:
        latex_in_original = notation
    to_print = _full_notation_string(
        main_note, notation, description, latex_in_original)
    # TODO: change this to use VaultNote method
    with open(notation_note.path(), 'w+', encoding='utf8') as notation_file:
        notation_file.write(to_print)
    if add_to_main:
        add_notation_note_to_see_also(notation_note, main_note)
    return notation_note
    

def _full_notation_string(
        main_note: VaultNote,
        notation: str,
        description: str,
        latex_in_original: str) -> str:
    """The full "statement" of a notation.
    
    Says something like "<notation> denotes <description of notation>", e.g.
    "$\dim V$ denotes the dimension of the vector space $V$".
    
    **Parameters**
    - notation - str
        - Notation written in LaTeX.
    - description - str
        - The full description of the notation.
        
    **Returns**
    - str
    """
    raw_notation = _raw_notation(notation)
    denote_link = ObsidianLink(False, main_note.name, 0, 'denotes')
    meta_latex_in_original = _raw_notation(
        latex_in_original).replace('\\', '\\\\')
    before_meta = _notation_string_no_metadata(
        raw_notation, denote_link, description)
    return (f'---\ndetect_regex: []\n'
            f'latex_in_original: ["{meta_latex_in_original}"]\n'
            f'tags: []'
            f'\n---\n{before_meta}')


def _raw_notation(notation: str):
    """
    """
    notation = notation.strip()
    notation = notation.strip('$')
    notation = notation.replace('\n', '')
    notation = notation.strip()
    return notation


def _notation_string_no_metadata(
        raw_notation: str,
        denote_link: ObsidianLink,
        description: str) -> str:
    """
    This is a helper function to `_full_notation_string`.
    """
    return f'${raw_notation}$ {str(denote_link)} {description}'



# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 27
MAX_NOTE_NAME_LENGTH = 80
def _make_notat_notes_from_sifted_notats(
        main_note: VaultNote, vault: PathLike, reference_name: str,
        notations: list[tuple[str, str]], destination: Optional[PathLike],
        overwrite: bool, add_to_main: bool) -> list[VaultNote]:
    """
    Create the notation notes based on notations 
    that were found in `main_note` (either surrounded by
    double asterisks `**` or within an HTML tag.)

    This is a helper function to `make_notation_notes_from_double_asts`
    and `make_notation_notes_from_HTML_tags`.
    """
    # TODO: test that note names aren't too long.
    new_notes = []
    for full, notation in reversed(notations):
        if not notation:
            notation = full
        notation_note_name = f'{reference_name}_notation_'\
            f'{latex_to_path_accepted_string(notation)}'
        if len(notation_note_name) > MAX_NOTE_NAME_LENGTH:
            notation_note_name = notation_note_name[:MAX_NOTE_NAME_LENGTH]
        notation_note_name = VaultNote.unique_name(
            notation_note_name, vault)
        new_note = make_a_notation_note(
            main_note, vault, notation, '', notation_note_name,
            destination, overwrite, add_to_main, latex_in_original=full)
        if new_note:
            new_notes.append(new_note)
    return new_notes
    

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 30
def make_notation_notes_from_double_asts(
        main_note: VaultNote, # The standard information note from which the notations are marked with double asterisks
        vault: PathLike, # The name of the reference; the notation note's name will start with `{reference_name}_notation_`.
        reference_name: str,
        destination: Optional[PathLike] = None, # The directory to create the new notation notes in.  If `None`, then creates the new notation note in the same place as the note specified by `note_name`
        overwrite: bool = False, # If `True`, overwrite file of the same path as the new notation file to be written, if such a file exists.  Otherwise, does nothing. Defaults to `False`.
        add_to_main: bool = True # If `True`, adds links to the notation note in the `See Also` section of the main note.
        ) -> list[VaultNote]: # The list of VaultNotes that are newly created/modified.
    """Make notation notes based on double asterisks surrounding LaTeX text in
    a standard information note.

    Notations are deemed to be completely LaTeX text in info notes that
    are surrounded by double asterisks. In basicality, if such a LaTeX
    text (without surrounding dollars signs `$` or `$$`) is listed in
    the `latex_in_original` metadata section of some notation note in the same
    directory as the info note whose main note is the info note in question,
    then a new notation note for that LaTeX text 
    is not created. However, if there are multiple instances of the same
    LaTeX text, then some notation notes may be created so that the number
    of times the LaTeX text appears in the info note is the no more than
    the number of times the LaTeX text appears in `latex_in_original` metadata
    sections of notation notes (in the same directory as the info note whose
    main note is the info note).

    For example, if there is an info note with notations `A`, `A`, `'A'`,
    `'A'`, and `B` and if there is a single notation note in the same
    directory as the info note with two `'A'` and `'A'` entries in its
    `latex_in_original` metadata section, then three notation notes will be
    created: two with `'A'` listed in their `latex_in_original` sections, and
    one with `'B'` listed in its `latex_in_original` section.

    **Raises**

    - Warning
        - If there are notation notes whose main note is determined to
        be to `main_note` and whose notations "excessively cover" those
        in `main_note`, i.e. the notation notes have more notations than
        `main_note` introduces. The main note and the excessive
        notations are printed; the notations are printed instead of the 
        notation notes because the same notation may span either multiple
        or single notation notes.
    """
    # Find notations
    notations = notat_str_from_doub_asts_in_std_info_note(main_note)
    notations = [_raw_notation(notation) for notation in notations]
    # Get only the notations not already made into notes based on
    # latex_in_original
    all_latex_in_original = _latex_in_original_from_notat_notes_to_main_note(
        vault, main_note)
    notations_to_create = Multiset(notations).difference(all_latex_in_original)
    notations_to_create = [(notat, "") for notat in notations_to_create]
    # Alert of existing notations that should not be there
    excess_notations = all_latex_in_original.difference(Multiset(notations))
    excess_notations = list(excess_notations)
    if excess_notations:
        warnings.warn(
            f"The following note has the following excess notations: "
            f"{main_note.name}, {', '.join(excess_notations)}")
    # Make notation notes
    return _make_notat_notes_from_sifted_notats(
        main_note, vault, reference_name, notations_to_create,
        destination, overwrite, add_to_main)
    

def _latex_in_original_in_notat(
        notation_note: VaultNote,
        default_to_notation_in_note: bool = True # If `True`, then the list with the single item `notation_note_note(notation_note)` is returned.
        ) -> list[str]:
    """Return the `latex_in_original` metadata section of the notation note.
    
    If the `latex_in_original` metadata section does not exist, then returns
    the list consisting of the notation in the notation note.
    """
    # TODO: test in the case that `latex_in_original` section does not exist
    mf = MarkdownFile.from_vault_note(notation_note)
    metadata = mf.metadata()
    if metadata is not None:
        return metadata.get('latex_in_original',
                            [notation_in_note(notation_note).strip('$')])
    elif default_to_notation_in_note:
        return [notation_in_note(notation_note)]
    else:
        return []

    
def _latex_in_original_from_notat_notes_to_main_note(
        vault: PathLike,
        main_note: VaultNote # The info note
        ) -> Multiset:
    """Return a Multiset enumerating the entries of `latex_in_original`
    in the notation notes in the same directory as an info note
    """
    notation_notes_in_folder = notations_and_main_notes(vault, note=main_note)
    notation_notes_of_main_note = [
        VaultNote(vault, name=notation_note) for notation_note, info_note
        in notation_notes_in_folder.items()
        if main_note.name == info_note]

    all_latex_in_original = Multiset()
    for notat_note in notation_notes_of_main_note:
        all_latex_in_original.update(_latex_in_original_in_notat(notat_note))
    return all_latex_in_original





# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 46
def make_notation_notes_from_HTML_tags(
        main_note: VaultNote, # The standard information note in which the notations are marked with HTML tags.
        vault: PathLike, 
        reference_name: str, # The name of the reference; the notation note's name will start with `{reference_name}_notation_`.
        destination: Optional[PathLike] = None, # The directory to create the new notation notes in.  If `None`, then creates the new notation note in the same place as the note specified by `note_name`
        overwrite: bool = False, # If `True`, overwrite file of the same path as the new notation file to be written, if such a file exists.  Otherwise, does nothing. Defaults to `False`.
        add_to_main: bool = True # If `True`, adds links to the notation note in the `See Also` section of the main note.
        ) -> list[VaultNote]: # The list of VaultNotes that are newly created/modified.
    r"""Make notation notes based on double asterisks surrounding LaTeX text
    in a standard information note.

    Notations are determined by HTML tags with the `notation`
    attribute in them. For instance, `<span notation="">$H^i$</span>`
    Is deemed to be a notation and the text of the notation note starts
    with "$H^i$ [[main_note.name|denotes]]".
    
    The `notation` attribute
    can also be used to specify more precisely the notation
    multiple notation notes, e.g. the tag
    `<span notation="h^i">$h^i := \dim_k H^i$</span>`
    begets a notation note that starts with "$h^i$ [[main_note.name|denotes]]".
    
    The `notation` attribute can also be used to beget multiple notation notes
    from a single HTML tag by separation by double semicolons `;;`. e.g. the
    tag

    `<span notation="IJ;;I+J">$$IJ = \langle ab: a \in I, b \in J \rangle, \quad I+J = \{a+b: a \in I, b \in J \}$</span>`

    begets two notation notes, one which starts with
    "$IJ$ [[main_note.name|denotes]]" and the other which starts with
    "$I+J$ [[main_note.name|denotes]]"

    **Raises**

    - Warning

        - If there are notation notes whose main note is determined to
        be to `main_note` and whose notations "excessively cover" those
        in `main_note`, i.e. the notation notes have more notations than
        `main_note` introduces. The main note and the excessive
        notations are printed; the notations are printed instead of the 
        notation notes because the same notation may span either multiple
        or single notation notes.

        - If there is a notation HTML tag surrounding text that is not a
        pure latex string.
    """
    # Find notations
    pairs_of_notat_strs = notat_str_from_html_tags(main_note)
    pairs_of_notat_strs = [(_raw_notation(full), actual) for full, actual in pairs_of_notat_strs]
    full_latex = [full for full, _ in pairs_of_notat_strs]
    # Get only the notations not already made into notes based on
    # latex_in_original
    all_latex_in_original = _latex_in_original_from_notat_notes_to_main_note(
        vault, main_note)
    notations_to_create = [
        (full, actual) for full, actual in pairs_of_notat_strs if full not in all_latex_in_original]
    # notations_to_create = Multiset(full_latex).difference(all_latex_in_original)
    # notations_to_create = list(notations_to_create)

    # Alert of existing notations that should not be there
    excess_notations = all_latex_in_original.difference(Multiset(full_latex))
    excess_notations = list(excess_notations)
    if excess_notations:
        warnings.warn(
            f"The following note has the following excess notations: "
            f"{main_note.name}, {', '.join(excess_notations)}")
    # Make notation notes
    return _make_notat_notes_from_sifted_notats(
        main_note, vault, reference_name, notations_to_create,
        destination, overwrite, add_to_main)

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 50
def notation_note_has_no_verified_content(
    notation_note: VaultNote,
    ) -> bool:
    r"""
    Return `True` if `notation_note` has no genuine content
    or if the content is determined to have been auto-generated.
    is determined to not
    have any genuine content or the content is auto-generated.

    """
    mf = MarkdownFile.from_vault_note(notation_note)
    _, _, _, content, links = parse_notation_note(notation_note)
    if not str(content).strip():
        return True
    # In the absence of the `_auto/notation_summary` tag, 
    # any content that is present is probably not auto-generated.
    if not mf.has_tag('_auto/notation_summary'):
        return False
    # If the notation note has a link, then its content is
    # probably not auto-generated.
    if links:
        return False
    return True

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 52
def remove_bad_notation_notes(
        main_note: VaultNote, # The standard information note in which the notations are marked with HTML tags and which notation notes are to be removed as appropriate.
        vault: PathLike, 
        # reference_name: str, # The name of the reference; the notation note's name will start with `{reference_name}_notation_`.
        # destination: Optional[PathLike] = None, # The directory to create the new notation notes in.  If `None`, then creates the new notation note in the same place as the note specified by `note_name`
        # overwrite: bool = False, # If `True`, overwrite file of the same path as the new notation file to be written, if such a file exists.  Otherwise, does nothing. Defaults to `False`.
        # add_to_main: bool = True, # If `True`, adds links to the notation note in the `See Also` section of the main note.
        links_in_vault: Optional[dict[str, list[str]]] = None, # An output to `all_links_in_vault` with `backlinks` set to `True`. If `None`, then this is computed on-the-fly. 
        ) -> list[VaultNote]: # The list of VaultNotes that are newly created/modified.
    r"""
    Remove "bad" notation notes associated to `main_note`

    A "bad" notation note is one which satisfies all of the
    following:

    1. is determined to essentially have no verified content (via
       the `notation_note_has_no_verified_content` function).
    2. is not linked to anything in `vault` except for `main_note`.
    # 3. all entries of the `latex_in_original` field in the YAML
       frontmatter meta are not present in `main_note`

    """
    if not links_in_vault:
        links_in_vault = all_links_in_vault(vault, backlinks=True)
    
    mf = MarkdownFile.from_vault_note(main_note)
    # heading_index = mf.get_line_number_of_heading(title='See Also')
    lines_to_remove = _remove_notation_notes(mf, vault, links_in_vault)
    for line_to_remove in reversed(lines_to_remove):
        mf.remove_line(line_to_remove)
    mf.write(main_note)


def _remove_notation_notes(
        mf: MarkdownFile, # The `MarkdownFile` object constructed from `main_note`
        vault: PathLike,
        links_in_vault: dict[str, list[str]]
        ) -> list[int]:
    """
    Find notation notes linked to the main notes, delete the bad ones,
    and return a list of indices for lines in `mf` corresponding
    to bulleted links in the `'See Also'` section.

    This is a Helper function to ```remove_bad_notation_notes```.

    """
    heading_index = mf.get_line_number_of_heading(title='See Also')
    lines_to_remove = []
    for ind, part in enumerate(mf.parts[heading_index+1:]):
        if part['type'] == MarkdownLineEnum.HEADING:
            break
        line_text = part['line']
        link_text = line_text.strip('- \n\t\r')
        try:
            link_object = ObsidianLink.from_text(link_text)
        except LinkFormatError as e:
            continue
        note_name = link_object.file_name
        linked_note = VaultNote(vault, name=note_name, update_cache=False)
        if not note_is_of_type(linked_note, PersonalNoteTypeEnum.NOTATION_NOTE):
            continue
        if _notation_note_is_bad(linked_note, links_in_vault):
            lines_to_remove.append(ind + heading_index + 1)
            linked_note.delete()
    return lines_to_remove





def _notation_note_is_bad(
        notation_note: VaultNote,
        links_in_vault: dict[str, list[str]],
        ) -> bool:
    r"""
    "Bad" notes are to be removed.

    This is a helper function to `remove_bad_notation_notes`.
    """
    if not notation_note_has_no_verified_content(notation_note):
        return False
    mf = MarkdownFile.from_vault_note(notation_note)
    # metadata = mf.metadata()
    # if 'latex_in_original' not in metadata:
    #     return False

    # latex_in_original_candidates = set(
    #     _latex_in_original_of_html_tags(str(mf)))
    # for entry in metadata['latex_in_original']:
    #     entry = entry.replace(r'\\', "\\")
    #     if entry in latex_in_original_candidates:
    #         return False

    if (notation_note.name in links_in_vault
            and len(links_in_vault[notation_note.name]) > 1):
        return False
    return True


# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 55
def _latex_in_original_of_html_tags(text: str) -> list[str]:
    r"""
    Each string represents the latex string
    (without surrounding dollar signs) encapsulated by the
    HTML tags with the 'notation' attribute.
    """
    _, html_tags = remove_html_tags_in_text(text)
    return [tag.getText().strip('$') for tag, _, _ in html_tags
            if 'notation' in tag.attrs.keys()]

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 56
def reorder_notation_note_links_in_see_also_section(
    main_note: VaultNote,
    vault: PathLike
    ) -> None:
    r"""
    Reorder the bulleted links to the notation notes
    in the `# See Also` section to match the order
    of the "latex in original" latex strings as they
    appear in `main_note`. 

    Assumes that the `# See Also` section begins with bulleted
    links to notation notes and that all bulleted links to
    notation notes in `main_note` reside at the beginning
    of the `# See Also` section.
    """
    mf = MarkdownFile.from_vault_note(main_note)
    heading_index = mf.get_line_number_of_heading(title='See Also')
    notation_note_names = []
    for ind, part in enumerate(mf.parts[heading_index+1:]):
        if part['type'] == MarkdownLineEnum.HEADING:
            break
        line_text = part['line']
        link_text = line_text.strip('- \n\t\r')
        try:
            link_object = ObsidianLink.from_text(link_text)
        except LinkFormatError as e:
            continue
        note_name = link_object.file_name
        linked_note = VaultNote(vault, name=note_name, update_cache=False)
        if note_is_of_type(linked_note, PersonalNoteTypeEnum.NOTATION_NOTE):
            notation_note_names.append(link_object.file_name)
        else:
            break
    notation_note_names = _order_of_notat_notes(mf, vault, notation_note_names)
    mf.remove_lines(heading_index + 1, ind + heading_index + 1)
    for name in reversed(notation_note_names):
        mf.add_line_in_section(
            'See Also',
            {'line': f'- [[{name}]]',
             'type': MarkdownLineEnum.UNORDERED_LIST})
    mf.write(main_note)
    


def _first_latex_in_original(
        notation_note: VaultNote):
    """
    Helper function to `reorder_notation_note_links_in_see_also_section`.
    """
    listy = _latex_in_original_in_notat(notation_note, default_to_notation_in_note=False)
    if listy:
        return listy[0].replace(r'\\', '\\')
    return None


def _get_index_of_latex_in_original(
        latex_in_original: str,
        notat_note_names_and_inds: dict[str, int]
        ) -> int:
    """
    Helper function to `reorder_notation_note_links_in_see_also_section`.
    """
    if latex_in_original in notat_note_names_and_inds:
        return notat_note_names_and_inds[latex_in_original]
    return -1


def _order_of_notat_notes(
        mf: MarkdownFile, # The `MarkdownFile` object constructed from mthe main note.
        vault: PathLike,
        notation_note_names: list[str]
        ) -> list[str]:
    """
    Return the names of the notation notes in the order
    that they should appear in the `# See Also` section
    of the main note.
    """
    notat_note_names_and_latex_in_original = [
        (name, _first_latex_in_original(VaultNote(vault, name=name)))
         for name in notation_note_names]
    _, html_tags = remove_html_tags_in_text(str(mf))
    latex_in_original_order = {}
    # for (tag, _, _), ind in enumerate(html_tags):
    #     latex_in_original_order[tag.getText().strip('$')] = ind
    latex_in_original_order = {
        tag.getText().strip('$'): ind
        for ind, (tag, _, _) in enumerate(html_tags)}
    notat_note_names_and_inds = [
        (name, _get_index_of_latex_in_original(
            latex_in_original, latex_in_original_order))
         for (name, latex_in_original) in notat_note_names_and_latex_in_original]
    notat_note_names_and_inds = sorted(notat_note_names_and_inds, key=lambda x: x[1])
    return [name for name, _ in notat_note_names_and_inds]

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 65
SPECIAL_CHARACTERS = ['.', '+', '*', '?', '^', '$', '(', ')',
                      '[', ']', '{', '}', '|', '\\']
replaceable_groups = [['mathrm', 'operatorname', 'rm', 'text'],
                      ['mathbf', 'bf'],
                      ['mathit', 'it']]


def _build_replacables_from_groups(
        replaceable_groups: list[list[str]]) -> dict[str, set[str]]:
    total_dict = {}
    for listy in replaceable_groups:
        set_for_group = set(listy)
        for macro in listy:
            total_dict[macro] = set_for_group
    return total_dict


REPLACEABLES = _build_replacables_from_groups(replaceable_groups)
    

def regex_from_latex(
        latex: str, replaceables: dict[str, set[str]] = REPLACEABLES,
        special_characters: list[str] = SPECIAL_CHARACTERS) -> str:
    """Returns regex to match latex math mode string which is essentially
    equivalent to a specified latex math mode string.
    
    The outputs of this function may not work correctly.
    The regex pattern does not have to fully match equivalent string.
    
    **Parameters**

    - latex - str
        - The latex math mode string. Does not include math mode delimiters
        such as `$`, `$$`, `\[ \]` (although the characters `'\['` and `'\]'`
        can still be part of the string, e.g. for optional arguments of a
        macro/operator). Can include "placeholders" `r'\1'`, `r'\2'`, `r'\3'`,
        etc. to indicate substitutable/generics; the placeholders can be
        substituted with any string.
    - replaceables - dict[str, set[str]]
        - latex strings/commands which are considered "interreplacable"
    - special_characters - list[str]
        - characters to add a backslash `'\'` in front of for regex.
        Defaults to a list consisting of special characters in regex.
    """
    if not replaceables:
        replaceables = {}
    w = LatexWalker(latex)
    nodelist, _, _ = w.get_latex_nodes(pos=0)
    regex_parts = []
    # print(nodelist)
    for node in nodelist:
        _look_into_node(node, regex_parts,
                        replaceables, special_characters)
    regex_parts.append('(?:[ \\{\\}]*)')
    return ''.join(regex_parts)
    
def _look_into_node(
        node: LatexNode, regex_parts: list[str],
        replaceables: dict[str, set[str]],
        special_characters: list[str]) -> None:
    """Appends to `regex_parts`"""
    # hasattr(node, 'nodeargd')
    # print(node)
    if isinstance(node, LatexMacroNode):
        macroname = node.macroname
        if _macro_is_actually_placeholder(macroname):
            regex_parts.append('(?:.*)')
        else:
            if macroname in replaceables:
                replaceable_macros = replaceables[macroname]
            else:
                replaceable_macros = [macroname]
            options_str = '|'.join(replaceable_macros)
            options_str = f'(?:{options_str})'
            regex_parts.append(fr'(?: *?)\\{options_str}(?: *?)')
        for node in node.nodeargd.argnlist:
            _look_into_node(node, regex_parts,
                            replaceables, special_characters)
    elif isinstance(node, LatexGroupNode):
        # print('\nGroup Node')
        # print(node)
        # print(node.nodelist)
        delimiters = node.delimiters
        regex_parts.append(f'\\{delimiters[0]}(?: *?)')
        for node in node.nodelist:
            _look_into_node(node, regex_parts,
                            replaceables, special_characters)
        regex_parts.append(f'(?: *?)\\{delimiters[1]}')
    elif isinstance(node, LatexCharsNode):
        # print('\nChars Node')
        # print(node)
        # print(node.chars)
        chars = node.chars.strip()
        chars = list(chars)
        chars = [f'\\{char}' if char in special_characters else char
                 for char in chars]
        # print(chars)
        chars.insert(0, '')  # add the misc spaces/brackets front and back
        chars.append('')
        regex_optional_spaces_and_brackets = '(?:[ \\{\\}]*?)'.join(chars)
        regex_parts.append(regex_optional_spaces_and_brackets)
        
def _macro_is_actually_placeholder(macro: str) -> bool:
    return macro.isnumeric()

# %% ../../../../../nbs/20_markdown.obsidian.personal.notation.ipynb 69
def regex_from_notation_note(vault: PathLike, note: VaultNote) -> str:
    """Returns a regex str to detect the notation of the notation note.
    
    The regex detection strings should be in a list labeled `detect_regex` in
    the yaml frontmatter. If multiple strings are in the list, then the regex
    will detect latex math mode strings roughly corresponding to any of them.
    If multiple strings are in the list, then they must be ordered 
    "by priority", with the higher priority regexes coming first. It is good
    to have these string in quotes `""` to make sure that yaml can load them
    safely. When doing so, make sure to escape characters, e.g. backslash
    should be typed as `\\`, etc.
    
    The strings in `detect_regex` can include placeholders, cf.
    ``regex_from_latex``.
    
    **Parameters**
    - vault - PathLike
    - note - VaultNote
    
    **Returns**
    - str
        - Of the regex used to detect the notation. The regex does not need to
        fully match instances of the notation.
    """
    assert note_is_of_type(note, PersonalNoteTypeEnum.NOTATION_NOTE)
    mf = MarkdownFile.from_vault_note(note)
    metadata = mf.metadata()
    if metadata and 'detect_regex' in metadata:
        detects = metadata['detect_regex']
        regexes = [regex_from_latex(detect) for detect in detects]
        return '|'.join(regexes)
    else:
        notation = notation_in_note(note, vault)
        return regex_from_latex(notation[1:-1])  # Get rid of `'$'`.
