"""Functions for gathering and processing data to train and for using  ML models to link notation notes with one another."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb.

# %% auto 0
__all__ = ['data_from_notation_notes', 'data_points_for_reference', 'text_from_data_point', 'prediction_by_model',
           'prediction_by_model_via_datapoint', 'auto_add_link_to_notation_note',
           'auto_add_link_to_notation_note_via_data_point']

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 5
from os import PathLike
import random
import re
from typing import Optional, Union

from ....markdown.file import MarkdownFile, MarkdownLineEnum
from ...links import MARKDOWNLINK_CAPTURE_PATTERN
from ..information_notes import reference_of_information_note
from ..notation.in_standard_information_note import notation_notes_linked_in_see_also_section
from ..notation.parse import parse_notation_note
from ..note_processing import process_standard_information_note
from ..note_type import note_is_of_type, type_of_note, PersonalNoteTypeEnum
from ..notes import notes_linked_in_notes_linked_in_note
from .notation_summarization import _notation_note_has_auto_summary_tag
from ...vault import VaultNote

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 9
# TODO: test
def _init_args_for_data_from_notation_notes(
        origin_notation_note: VaultNote, # The notation note which potentially uses the notation introduced by `relied_notation_note`. In particular, there potentially ought to be a link to `relied_notation_note` in `origin_notation_note`.`
        relied_notation_note: VaultNote, # See the description for `origin_notation_note`.`,
        origin_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `origin_notation_note`
        relied_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `relied_notation_note`
        reference_name: Optional[str] = None, # The name of the reference folder in the vault from which the two notation notes comes from. If `None`, this is computed "on-the-fly" based on the reference of the main note of main_of_origin, see `reference_of_information_note`
        main_of_origin_content: Optional[str] = None, # The content of `main_of_origin`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_origin))`. If None, this is computed "one-the-fly".
        main_of_relied_content: Optional[str] = None, # The content of `main_of_relied`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_relied))`. If None, this is computed "one-the-fly".
        information_notes_of_reference: Optional[list[VaultNote]] = None, # The standard information notes for the reference folder in order (as arranged in the index notes of the reference folder)
    ) -> tuple[str, str, Union[list[VaultNote], None], VaultNote, VaultNote, str, str, list[VaultNote]]  :

    if origin_parsed is None:
        origin_parsed = parse_notation_note(origin_notation_note)
    if relied_parsed is None:
        relied_parsed = parse_notation_note(relied_notation_note)

    main_of_origin, main_of_relied = origin_parsed[2], relied_parsed[2]
    if reference_name is None and main_of_origin is not None:
        reference_name = reference_of_information_note(main_of_origin)

    if main_of_origin_content is None and main_of_origin is not None:
        main_of_origin_content = process_standard_information_note(
            MarkdownFile.from_vault_note(main_of_origin))
    if main_of_relied_content is None and main_of_relied is not None:
        main_of_relied_content = process_standard_information_note(
            MarkdownFile.from_vault_note(main_of_relied))

    if information_notes_of_reference is None:
        # TODO Implement the initialization of `information_notes_of_reference`.
        information_notes_of_reference = information_notes_of_reference
    return (
        origin_parsed, relied_parsed, reference_name, main_of_origin_content,
        main_of_relied_content, information_notes_of_reference)



# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 10
# TODO: test
def _notat_str(
        meta: Union[dict, None],
        notat_str: str
    ) -> str:
    r"""Get the closest thing to the original latex string from which a notation note
    is derived from based on the note's YAML frotmatter metadata.

    If the metadata has a `latex_in_original` field, then return its first entry.
    Otherwise, return `notat_str`, which is the string such that the notation note
    starts with the formatting `$<notat_str>$ [[<link_to_main_note>|denotes]]...`.

    This is a helper function to `data_from_notation_notes`.
    """
    if meta and 'latex_in_original' in meta and len(meta['latex_in_original']) > 0:
        return meta['latex_in_original'][0]
    else:
        return notat_str


def _origin_links_to_relied(
        linked_notat_notes: list[tuple], # One of the outputs of `parse_notation_note`
        origin_content: MarkdownFile,  # One of the outputs of `parse_notation_note`
        relied_notation_note: VaultNote
        ) -> bool:
    r"""Returns `True` if the origin notation note links to `relied_notation_note`.

    Helper function to `data_from_notation_notes`.
    
    This function checks both the trailing list of links as well as any links within
    the content of the origin notation note.
    """

    linked_note_names = []
    for _, notat_note_name in linked_notat_notes:
        if notat_note_name.endswith('.md'):
            notat_note_name = notat_note_name[:-3]
        linked_note_names.append(notat_note_name)
        # if notat_note_name == relied_notation_note.name:
        #     return True
    linked_note_names.extend(
        _linked_note_names_from_content(str(origin_content)))
        
    for note_name in linked_note_names:
        if note_name == relied_notation_note.name:
            return True
    return False


def _adjust_content(
        include_content: bool, 
        content: str,
        meta: Union[dict, None],
        ) -> str:
    r"""
    Helper function to `data_from_notation_notes`.

    Sometimes, the content of a notation note is autogenerated by an ML model
    and hence can be unreliable. This function is used to exclude
    autogenerated content.
    """
    if not include_content or (
            meta is not None and 'tags' in meta and '_auto/notation_summary' in meta['tags']):
        return ""
    return content


def _linked_note_names_from_content(content: str):
    linked_note_names = []
    for match in re.findall(MARKDOWNLINK_CAPTURE_PATTERN, content):
        link_name = match[1]
        if link_name.endswith('.md'):
            link_name = link_name[:-3]
        linked_note_names.append(link_name)
    return linked_note_names
        

def _linked_notat_note_names_from_content(content: str, vault: PathLike):
    linked_note_names = _linked_note_names_from_content(content)
    linked_notation_note_names = []
    for linked_note_name in linked_note_names:
        note = VaultNote(vault, name=linked_note_name, update_cache=False)
        if note_is_of_type(note, PersonalNoteTypeEnum.NOTATION_NOTE):
            linked_notation_note_names.append(note.name)
    return linked_notation_note_names

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 11
# TODO: test
def data_from_notation_notes(
        origin_notation_note: VaultNote, # The notation note which potentially uses the notation introduced by `relied_notation_note`. In particular, there potentially ought to be a link to `relied_notation_note` in `origin_notation_note`.`
        relied_notation_note: VaultNote, # See the description for `origin_notation_note`.`,
        include_origin_content: bool, # If `True`, include the content of `origin_notation_note`, i.e. a summary of what the notation introduced by this note means.
        include_relied_content: bool, # If `True`, include the content of `relied_notation_note`, i.e. a summary of what the notation introduced by this note means.
        origin_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `origin_notation_note`
        relied_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `relied_notation_note`
        reference_name: Optional[str] = None, # The name of the reference folder in the vault from which the two notation notes comes from. If `None`, this is computed "on-the-fly" based on the reference of the main note of main_of_origin, see `reference_of_information_note`
        main_of_origin_content: Optional[str] = None, # The content of `main_of_origin`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_origin))`. If None, this is computed "one-the-fly".
        main_of_relied_content: Optional[str] = None, # The content of `main_of_relied`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_relied))`. If None, this is computed "one-the-fly".
        information_notes_of_reference: Optional[list[VaultNote]] = None, # The standard information notes for the reference folder in order (as arranged in the index notes of the reference folder)
    ) -> tuple[str, str, str, str, str, str, str, str, str, str, str, str, str, bool]:
    """
    Obtain data for a single pair of notation notes.

    Assumes that

    - `origin_notation_note` and `relied_notation_note` have the same `vault`
      attribute.
    - `origin_parsed` and `relied_parsed` are respectively the outputs of
      `parse_notation_note` applied to `reference_name` if specified.
    - `reference_name` is the correct output of `reference_of_information_note`
      applied to `main_of_origin` and that this output is the same as that when
      applied to `main_of_relied`.
    - `main_of_origin_content` and `main_of_relied_content` are the outputs of
      `process_standard_information_note(MarkdownFile.from_vault_note(main_of_origin))`
      and
      `process_standard_information_note(MarkdownFile.from_vault_note(main_of_relied))`
      respectively if they are specified.
    - `information_notes_of_reference` correctly lists the standard information
      notes from the reference in the vault of name `reference_name`.

    **Returns**
    - tuple[str, str, str, str, str, str, str, str, str, str, str, str, str, bool] -
      Consists of the following:
        1. The name of the reference from which the notation notes and main information
           notes come from,
        2. The name of `origin_notation_note`,
        3. The name of `relied_notation_note`,
        4. The name of the main information note of `origin_notation_note`,
        5. The name of the main information note of `relied_notation_note`,
        6. The (processed) content of `main_of_origin_content`,
        7. The (processed) content of `main_of_relied_content`,
        8. The content of `origin_notation_note`
        9. The content of `relied_notation_note`
        10. The first entry in the `latex_in_original` field in the YAML frontmatter
           metadata or, if unavailable, the notation that is summarized in
           `origin_notation_note`
        11. The first entry in the `latex_in_original` field in the YAML frontmatter
           metadata or, if unavailable, the notation that is summarized in
           `relied_notation_note`
        12. The notation that is summarized in `origin_notation_note`
        13. The notation that is summarized in `relied_notation_note`
        14. `True` if `origin_notation_note` links to `relied_notation_note`.
            `False` otherwises


    """
    (origin_parsed, relied_parsed, reference_name, main_of_origin_content,
     main_of_relied_content, information_notes_of_reference
     ) = _init_args_for_data_from_notation_notes(
         origin_notation_note, relied_notation_note, 
         origin_parsed, relied_parsed,
         reference_name, main_of_origin_content,
         main_of_relied_content, information_notes_of_reference)
     
    (origin_meta, origin_notat_str, main_of_origin_name,
     origin_content, linked_notat_notes
     ) = parse_notation_note(origin_notation_note)
    (relied_meta, relied_notat_str, main_of_relied_name, relied_content, _
     ) = parse_notation_note(relied_notation_note)
    origin_content, relied_content = str(origin_content), str(relied_content)
    origin_content = _adjust_content(include_origin_content, origin_content, origin_meta)
    relied_content = _adjust_content(include_relied_content, relied_content, relied_meta)

    return (
        reference_name, origin_notation_note.name, relied_notation_note.name,
        main_of_origin_name, main_of_relied_name,
        main_of_origin_content, main_of_relied_content,
        origin_content, relied_content,
        _notat_str(origin_meta, origin_notat_str),
        _notat_str(relied_meta, relied_notat_str),
        origin_notat_str, relied_notat_str,
        _origin_links_to_relied(linked_notat_notes, origin_content, relied_notation_note)
        )



# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 12
def data_points_for_reference(
        reference_index_note: VaultNote, # The index note for the reference from which to draw the data.
        return_notation_note_parsings: bool = False, # If `True`, return the outputs of `parse_notation_note` applied to the notation notes in the reference folder 
        ) -> Union[list[tuple], tuple[list[tuple], dict[str, tuple]]]:
    r"""Compile data points for notation note linking based on the information
    notes and notation notes in a reference folder.

    "Positive" linking data points are relatively rare in comparison to "Negative"
    data points, so "Negative" data points are randomly sampled (although the random
    samples will redundantly include "Positive" data as well).

    Note that it makes sense to draw data exclusively within each "reference" ---
    notations tend to have dependencies within the same mathematical text.

    **Returns**
    - `Union[list[tuple], tuple[list[tuple], dict[str, tuple]]]`
        - Either 1. a list of tuples --- in this case, each tuple is a
          "data point" and is an output of `data_from_notation_notes` ---
          or 2. the list of tuples along with a dict whose keys are
          the names of the notation notes and whose values are the
          outputs of `parse_notation_note` applied to these notation notes.
    """
    info_notes = notes_linked_in_notes_linked_in_note(reference_index_note, as_dict=False)
    info_notes = [note for note in info_notes if note.exists() and note_is_of_type(note, PersonalNoteTypeEnum.STANDARD_INFORMATION_NOTE)]
    notat_notes = []
    for info_note in info_notes:
        notat_notes.extend(notation_notes_linked_in_see_also_section(info_note, info_note.vault))

    vault = reference_index_note.vault
    info_notes_and_processed_content = {}
    for note in info_notes:
        try:
            info_notes_and_processed_content[note.name] = str(process_standard_information_note(MarkdownFile.from_vault_note(note), vault))
        except TypeError as e:
            print(f"An error occurred while trying to process the following note: {note.name}")
            info_notes_and_processed_content[note.name] = note.text()
    # info_notes_and_processed_content = {
    #     note.name: str(process_standard_information_note(
    #         MarkdownFile.from_vault_note(note), vault))
    #     for note in info_notes}
    notation_notes_and_parsed = {
        notat_note.name: parse_notation_note(notat_note) for notat_note in notat_notes}

    # Notation notes whose summaries are written and not auto-generated
    confirmed_summary_notat_note_names = [
        notat_note for notat_note, parsed in notation_notes_and_parsed.items()
        if not _notation_note_has_auto_summary_tag(parsed[0])]

    # Get all positive pairs of notation notes
    positive_linked_notat_note_pairs = _positive_pairs_of_notation_notes(
        confirmed_summary_notat_note_names, notation_notes_and_parsed, vault)
    data_points = _positive_data_points(
        reference_index_note, positive_linked_notat_note_pairs,
        notation_notes_and_parsed, info_notes_and_processed_content)
    data_points.extend(_sample_data_points(
        reference_index_note, len(data_points)*4,
        notat_notes, confirmed_summary_notat_note_names, notation_notes_and_parsed,
        info_notes_and_processed_content))
    if return_notation_note_parsings:
        return data_points, notation_notes_and_parsed
    else:
        return data_points



def _positive_pairs_of_notation_notes(
        confirmed_summary_notat_note_names: list[str],
        notation_notes_and_parsed: dict[str, tuple],
        vault: PathLike
        ) -> list[tuple[str, str]]:
    r"""Return the pairs `(<origin_notat_note_name>, <linked_notat_note_name>)`
    where `origin_notat_note_name` is the name of a notation note whose notation
    summary is "confirmed" (i.e. written and not autogenerated) and where
    `linked_notat_note_name` is the name of a notation note linked by the
    notation note with name `origin_notat_note_name`.
    """
    positive_linked_notat_note_pairs = []
    for notat_note_name in confirmed_summary_notat_note_names:
        parsed = notation_notes_and_parsed[notat_note_name]
        for _, linked_notat_note_name in parsed[4]:
            if linked_notat_note_name.endswith('.md'):
                linked_notat_note_name = linked_notat_note_name[:-3]
            positive_linked_notat_note_pairs.append(
                (notat_note_name, linked_notat_note_name))
        for linked_notat_note_name in _linked_notat_note_names_from_content(
                str(parsed[3]), vault):
            positive_linked_notat_note_pairs.append(
                (notat_note_name, linked_notat_note_name))

    return positive_linked_notat_note_pairs
    

def _positive_data_points(
        reference_index_note: VaultNote,
        positive_linked_notat_note_pairs: list[tuple[str, str]],
        notation_notes_and_parsed: dict[str, tuple],
        info_notes_and_processed_content: dict[str, str]
        ) -> list[tuple]:
    r"""Gather data points for the pairs `(<origin_notat_note_name>, <linked_notat_note_name>)`
    where `origin_notat_note_name` is the name of a notation note whose notation
    summary is "confirmed" (i.e. written and not autogenerated) and where
    `linked_notat_note_name` is the name of a notation note linked by the
    notation note with name `origin_notat_note_name`.
    """
    vault = reference_index_note.vault
    data_points = []
    for origin_notat_note_name, linked_notat_note_name in positive_linked_notat_note_pairs:
        try:
            origin_notat_note = VaultNote(vault, name=origin_notat_note_name, update_cache=False)
            linked_notat_note = VaultNote(vault, name=linked_notat_note_name, update_cache=False)
            main_of_origin_name = notation_notes_and_parsed[origin_notat_note_name][2]
            main_of_linked_name = notation_notes_and_parsed[linked_notat_note_name][2]
            if (main_of_origin_name is None or main_of_linked_name is None
                    or main_of_origin_name not in info_notes_and_processed_content
                    or main_of_linked_name not in info_notes_and_processed_content):
                continue

            for include_origin_content, include_linked_content in [(True, True), (True, False), (False, True), (False, False)]:
                data_points.append(data_from_notation_notes(
                    origin_notat_note, linked_notat_note,
                    include_origin_content, include_linked_content, 
                    notation_notes_and_parsed[origin_notat_note_name], notation_notes_and_parsed[linked_notat_note_name],
                    reference_index_note.name[7:], 
                    info_notes_and_processed_content[main_of_origin_name], info_notes_and_processed_content[main_of_linked_name],
                    None  # TODO: Pass proper argument for information_notes_of_reference
                    ))
        except Exception as e:
            print('An error has occurred while gathering data:')
            print(e)
    return data_points


def _sample_data_points(
        reference_index_note: VaultNote,
        num_samples: int, 
        # info_notes: list[VaultNote],
        notat_notes: list[VaultNote],
        confirmed_summary_notat_note_names: list[VaultNote],
        notation_notes_and_parsed: dict[str, tuple],
        info_notes_and_processed_content: dict[str, str]
        ) -> list[tuple]:
    r"""Randomly sample pairs `(<origin_notat_note_name>, <relied_notat_note_name>)`
    """
    # num_samples = min(num_samples, len(confirmed_summary_notat_note_names))
    origins = random.choices(confirmed_summary_notat_note_names, k=num_samples)
    origins = [VaultNote(reference_index_note.vault, name=name, update_cache=False) for name in origins]
    relieds = random.choices(notat_notes, k=num_samples)
    data_points = []
    for origin, relied in zip(origins, relieds):
        main_of_origin_name = notation_notes_and_parsed[origin.name][2]
        main_of_relied_name = notation_notes_and_parsed[relied.name][2]
        if (main_of_origin_name is None or main_of_relied_name is None
                or main_of_origin_name not in info_notes_and_processed_content
                or main_of_relied_name not in info_notes_and_processed_content):
            continue
        for include_origin_content, include_relied_content in [(True, True), (True, False), (False, True), (False, False)]:
            data_points.append(data_from_notation_notes(
                origin, relied,
                include_origin_content, include_relied_content, 
                notation_notes_and_parsed[origin.name], notation_notes_and_parsed[relied.name],
                reference_index_note.name[7:], 
                info_notes_and_processed_content[main_of_origin_name], info_notes_and_processed_content[main_of_relied_name],
                None  # TODO: Pass proper argument for information_notes_of_reference
                ))
    return data_points

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 13
def text_from_data_point(
        data_point: tuple  # An output of `data_from_notation_notes`.
        ) -> str:
    r"""
    Format a data point to present it as a str.
    """
    text = (f"latex_in_original for origin_notation_note: {data_point[9]}\n"
            f"latex_in_original for relied_notation_note: {data_point[10]}\n"
            f"Notation for origin_notation_note: {data_point[11]}\n"
            f"Notation for relied_notation_note: {data_point[12]}\n"
            f"Summary for origin_notation_note: {data_point[7]}\n"
            f"Summary for relied_notation_note: {data_point[8]}\n\n\n\n")
    content_origin = f"Content for main note of origin_notation_note: {data_point[5]}"
    content_relied = _content_relied(data_point[5], data_point[6])
    return f"{text}{content_origin}\n\n\n\n{content_relied}"


def _content_relied(
        main_of_origin_content: str,
        main_of_relied_content: str,
        ) -> str:
    if main_of_origin_content == main_of_relied_content:
        return f"Content for main note of relied_notation_note: same as that of main note of origin_notation_note" 
    else:
        return f"Content for main note of relied_notation_note: {main_of_relied_content}"

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 15
def prediction_by_model(
        origin_notation_note: VaultNote, # 
        relied_notation_note: VaultNote, #
        learn, # The fastai textlearner which makes the prediction. 
        include_origin_content: bool, # If `True`, include the content of `origin_notation_note`, i.e. a summary of what the notation introduced by this note means.
        include_relied_content: bool, # If `True`, include the content of `relied_notation_note`, i.e. a summary of what the notation introduced by this note means.
        origin_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `origin_notation_note`
        relied_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `relied_notation_note`
        reference_name: Optional[str] = None, # The name of the reference folder in the vault from which the two notation notes comes from. If `None`, this is computed "on-the-fly" based on the reference of the main note of main_of_origin, see `reference_of_information_note`
        main_of_origin_content: Optional[str] = None, # The content of `main_of_origin`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_origin))`. If None, this is computed "one-the-fly".
        main_of_relied_content: Optional[str] = None, # The content of `main_of_relied`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_relied))`. If None, this is computed "one-the-fly".
        information_notes_of_reference: Optional[list[VaultNote]] = None, # The standard information notes for the reference folder in order (as arranged in the index notes of the reference folder)
        ) -> bool: # `True` if `model` predicts that `origin_notat_note` depends on the notation summarized by `relied_notat_note` and hence should link to `relied_notat_note`.
    r"""
    Predict whether a notation note depends on the notation
    summarized by another notation note.

    See `data_from_notation_notes` for details on the parameters, except `learn`.

    See also `prediction_by_model_via_datapoint` for an alternative
    function for predictions.
    """
    data_point = data_from_notation_notes(
        origin_notation_note, relied_notation_note, include_origin_content,
        include_relied_content, origin_parsed, relied_parsed, reference_name,
        main_of_origin_content, main_of_relied_content,
        information_notes_of_reference)
    input = text_from_data_point(data_point)
    pred, loss, _ = learn.predict(input)
    return pred == 'True'


def prediction_by_model_via_datapoint(
        learn, # The fastai textlearner which makes the prediction. 
        data_point: tuple, # An output of `data_from_notation_notes`
        ) -> bool:
    r"""

    """
    input = text_from_data_point(data_point)
    pred, loss, _ = learn.predict(input)
    return pred == 'True'

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 16
# TODO: mark the note with an _auto tag and make it so that data collection doesn't pick
# up auto-generated links.
def auto_add_link_to_notation_note(
        origin_notation_note: VaultNote,
        relied_notation_note: VaultNote,
        learn, # The fastai textlearner which makes the prediction. 
        include_origin_content: bool, # If `True`, include the content of `origin_notation_note`, i.e. a summary of what the notation introduced by this note means.
        include_relied_content: bool, # If `True`, include the content of `relied_notation_note`, i.e. a summary of what the notation introduced by this note means.
        origin_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `origin_notation_note`
        relied_parsed: Optional[tuple] = None, # The output of `parse_notation_note` applied to `relied_notation_note`
        reference_name: Optional[str] = None, # The name of the reference folder in the vault from which the two notation notes comes from. If `None`, this is computed "on-the-fly" based on the reference of the main note of main_of_origin, see `reference_of_information_note`
        main_of_origin_content: Optional[str] = None, # The content of `main_of_origin`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_origin))`. If None, this is computed "one-the-fly".
        main_of_relied_content: Optional[str] = None, # The content of `main_of_relied`, i.e. the output of `process_standard_information_note(MarkdownFile.from_vault_note(main_of_relied))`. If None, this is computed "one-the-fly".
        information_notes_of_reference: Optional[list[VaultNote]] = None, # The standard information notes for the reference folder in order (as arranged in the index notes of the reference folder)
        ) -> None:
    r"""
    Predict whether `origin_notation_note` depends on the notation
    summarized by `relied_notation_note` and add a link in
    `origin_notation_note` to `relied_notation_note` if so.

    It is recommended to pass precomputed arguments for
    the optional parameters. In particular, passing a precomputed
    argument to `origin_parsed` prevents the link to
    `relied_notation_note` from being redundantly added if such
    a link already exists.

    If a link to `relied_notation_note` is added, then an
    `#_auto/notation_notes_linked` tag is added to
    `origin_notation_note` if such a tag is not already
    present.

    """
    if _origin_notation_note_already_has_link_to_relied(
            relied_notation_note, origin_parsed):
        return
    pred = prediction_by_model(
        origin_notation_note, relied_notation_note, learn,
        include_origin_content,
        include_relied_content, origin_parsed, relied_parsed, reference_name,
        main_of_origin_content, main_of_relied_content,
        information_notes_of_reference)
    if not pred:
        return
    _add_notation_link(origin_notation_note, relied_notation_note, relied_parsed)


def _origin_notation_note_already_has_link_to_relied(
        relied_notation_note: VaultNote,
        origin_parsed: Union[tuple, None],
        ) -> bool:
    r"""
    Return `True` if `relied_notation_note is determined to be linked by
    `origin_notation_note` based on the contents of `origin_parsed`, which
    is the output of `parse_notation_note` appleid to `origin_notation_note`.

    This is a helper function to 
    `automatically_add_bulleted_link_to_other_notation_note`.
    """
    for _, linked_note_name in origin_parsed[4]:
        if linked_note_name == relied_notation_note.name:
            return True
    return False


def _add_notation_link(
        origin_notation_note: VaultNote,
        relied_notation_note: VaultNote,
        relied_parsed: tuple,
        ) -> None:
    r"""
    Add a link in `origin_notation_note` to `relied_notation_note`
    and add the tag `_auto/notation_notes_linked` to `origin_notation_note`.
    """
    if not relied_parsed:
        relied_parsed = parse_notation_note(
            relied_notation_note, relied_notation_note.vault)
    mf = MarkdownFile.from_vault_note(origin_notation_note)
    mf.add_tags('_auto/notation_notes_linked',
                enquote_entries_in_metadata_fields=['latex_in_original'])
    bullet = f'- [{relied_parsed[1]}]({relied_notation_note.name}.md)'
    mf.add_line_to_end(
        {'line': bullet,
         'type': MarkdownLineEnum.UNORDERED_LIST})
    mf.write(origin_notation_note)

# %% ../../../../../nbs/34_markdown.obsidian.machine_learning.notation_linking.ipynb 17
def auto_add_link_to_notation_note_via_data_point(
        learn, # The fastai textlearner which makes the prediction. 
        vault: PathLike, 
        data_point: tuple, # An output of `data_from_notation_notes`
        origin_parsed: Optional[tuple] = None, # Optionally, the output of `parse_notation_note` applied to the `origin_notation_note`.
        relied_parsed: Optional[tuple] = None, # Optionally, the output of `parse_notation_note` applied to the `relied_notation_note`.
        ) -> None:
    origin_notation_note = VaultNote(vault, name=data_point[1])
    relied_notation_note = VaultNote(vault, name=data_point[2])
    if origin_parsed is None:
        origin_parsed = parse_notation_note(origin_notation_note, vault)
    if relied_parsed is None:
        relied_parsed = parse_notation_note(relied_notation_note, vault)
    if _origin_notation_note_already_has_link_to_relied(
            relied_notation_note, origin_parsed):
        return
    pred = prediction_by_model_via_datapoint(learn, data_point)
    if not pred:
        return
    _add_notation_link(
        origin_notation_note, relied_notation_note,
        relied_parsed)
