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

# %% auto 0
__all__ = ['subsections_listed_in_index_note', 'subsection_folders', 'get_alphanumeric', 'correspond_headings_with_folder',
           'information_notes_linked_in_index_note', 'move_information_notes_to_correct_folder',
           'move_information_notes_to_correct_folder_for_all_indices', 'convert_title_to_folder_name',
           'convert_heading_to_folder_name', 'make_folders_from_index_note_headers', 'get_notes_from_index_note',
           'add_link_in_index_note_after_note_link']

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 3
import glob
import os
from os import PathLike
from pathlib import Path
import re
from typing import Union

from natsort import natsorted
from pathvalidate import sanitize_filename

from trouver.markdown.markdown.file import (
    MarkdownFile, MarkdownLineEnum
)
from ...markdown.heading import heading_title
from trouver.markdown.obsidian.links import (
    find_links_in_markdown_text, ObsidianLink, links_from_text
)
from trouver.markdown.obsidian.vault import (
    VaultNote, note_name_unique, note_path_by_name
)

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 6
def subsections_listed_in_index_note(
        index_note: Union[VaultNote, str], # The index note
        vault: PathLike
        ) -> dict[Union[int, str], [dict, str]]: # The keys are 1. line numbers and 2. `'title'`. The values are dict and str (the blank str if root node), respectively.
    """
    Return subsections/subchapters as listed in the index note

    **See Also**
    
    - The `get_headings_tree` function of the `MarkdownFile` class.
    """
    vault = Path(vault)
    if isinstance(index_note, str):
        index_note = VaultNote(vault, name=index_note)
    mf_file = MarkdownFile.from_vault_note(index_note)
    return mf_file.get_headings_tree()

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 8
def subsection_folders(
        index_note: Union[VaultNote, str], # The index note
        vault: PathLike,
        output_type: str, # `'absolute_path'`, `'relative_path'`, or `'name'`
        ) -> list[str]: # List of immediate subdirectories in the directory containing the index note.
    """
    Return subdirectories corresponding to subsections/subchapters, i.e.
    the folders in the same directory as the index note.

    The folders are arranged in the order specified by `natsorted`.
    """
    vault = Path(vault)
    if isinstance(index_note, str):
        index_note = VaultNote(vault, name=index_note)
    parent_directory = (vault / index_note.rel_path).parent
    # print(str(parent_directory))
    glob_result = natsorted(glob.glob(str(parent_directory) + '/**/'))
    if output_type == 'absolute_path':
        return glob_result
    elif output_type == 'relative_path':
        return [str(Path(dir).relative_to(vault)) for dir in glob_result]
    elif output_type == 'name':
        return [Path(dir).name for dir in glob_result]

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 12
def get_alphanumeric(
        title: str, # The title of either a folder or a heading. Must start with an alphanumeric.
        title_type: str # Either `folder` or `heading`.
        ) -> str: # An alphabet or a numeric (arabic or roman)
    """
    Get the alphanumeric of a title of either a folder or a heading
    in an index noteh.

    Assumes that each folder is titled
    `'{alphanumeric}_{folder_title}'` and each heading is titled
    `'{alphanumeric}. {heading_title}'`
    """
    assert title_type in ['folder', 'heading']
    if title_type == 'folder':
        return re.sub(r'(.*?)_.*' , r'\1', title)
    else:
        return re.sub(r'(.*?)\. .*', r'\1', title)
    


# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 14
def correspond_headings_with_folder(
        index_note: VaultNote,
        vault: PathLike,
        include_non_heading: bool = True # If `True`, and if there is text before any heading, then treat such text as being under a "blank" heading.
        ) -> dict[str, str]:
    """
    Return tuples of corresponding headings in an index note
    with folder names.
    
    Assumes that each folder is titled
    `'{alphanumeric}_{folder_title}'` and each heading is titled
    `'{alphanumeric}. {heading_title}'`
    
    **Returns**
    - dict[str, str]
        - Each key is a str indexing the headings and folders. The keys
        are usually alphanumerics (arabic or roman), depending on the
        numbering system of chapters/sections of the reference/text.
        The values are tuples `(folder_title, heading_title)` without 
        the alphanumeric. For the blank heading, the key/index, the folder title,
        and the heading title are all the empty str.
    """
    index = MarkdownFile.from_vault_note(index_note)
    headings = index.get_headings(levels=1)
    headings = [heading_title(heading) for heading in headings]
    folders = subsection_folders(index_note, vault, output_type='name')
    correspond_dict = {get_alphanumeric(heading, 'heading'): (heading, folder)
                       for heading, folder in zip(headings, folders)}
    # TODO do a better job at the conditional below; 
    # for example, consider the start of the text blank if it's just empty lines with spaces.
    if (include_non_heading and index.parts
            and index.parts[0]['type'] != MarkdownLineEnum.HEADING):
        correspond_dict[''] = ('', '')
    return correspond_dict
    

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 19
def information_notes_linked_in_index_note(
        index_note: VaultNote, # The note indexing the information notes.
        vault: PathLike,
        hints: list[PathLike] = None # Hints on where the information notes are likely to be found at.  Each path is relative to `vault` and points to a directory. Defaults to `None`.
        ) -> dict[str, list[VaultNote]]: # Each key is the index for the heading (usually either an alphanumerical or a roman numerical). Each value is a list of the information notes linked in the index note.
    """Find information notes to be moved to the correct folder.
    
    Current implementation just looks at level 1 headings.
    This function is used in `move_information_notes_to_correct_folder`.
    Assumes that all notes in the vault have unique names.
    """
    parent_folder = os.path.dirname(index_note.rel_path)
    headings_folders = correspond_headings_with_folder(index_note, vault)
    mf = MarkdownFile.from_vault_note(index_note)
    headings_text = mf.get_headings_and_text(levels=1, include_start=True)
    headings_text = {heading_title(heading): text for heading, text
                     in headings_text.items()}
    text_under_headings = {heading_index: headings_text[heading] 
                            for heading_index, (heading, _) in headings_folders.items()}
    links_by_headings = {heading_index:links_from_text(text) for
                         heading_index, text in text_under_headings.items()}
    note_names_by_headings = {heading_index:[il.file_name for il in links] 
                              for heading_index, links in links_by_headings.items()}
    # Find notes by headings, but also pass the folder corresponding to the heading
    # as a hint of where to find the note for speedup, in case the note is 
    # already at the right place.
    folders_by_index = {heading_index: Path(vault) / parent_folder / heading_folder 
                        for heading_index, (_, heading_folder) in headings_folders.items()}
    if not hints:
        hints = []
    notes_by_headings = {heading_index: [VaultNote(vault, name=nn, hints=hints+[folders_by_index[heading_index]]) 
                                         for nn in note_names]
                         for heading_index, note_names in note_names_by_headings.items()}
    return notes_by_headings
    

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 21
def move_information_notes_to_correct_folder(
        index_note: VaultNote,
        vault: PathLike,
        hints: list[PathLike] = None # Hints on where the information notes are likely to be found at.  Each path is relative to `vault` and points to a directory. Defaults to `None`.
        ) -> None:
    """Moves the information notes indexed by `index_note` to the correct folder.

    The "correct folder" is a folder in the same directory as `index_note`
    corresponding to the heading under which the information note is indexed.
    The current implementation just looks at level 1 headings.
    """
    parent_folder = os.path.dirname(index_note.path(relative=True))
    linked_notes = information_notes_linked_in_index_note(index_note, vault, hints)
    headings_folders = correspond_headings_with_folder(index_note, vault)
    for heading_index, notes in linked_notes.items():
        _move_notes_under_heading(heading_index, notes,
                                  parent_folder, headings_folders)


def _move_notes_under_heading(
        heading_index, notes: list[VaultNote], parent_folder, headings_folders):
    destination_folder = headings_folders[heading_index][1]
    for note in notes:
        note_folder = os.path.dirname(note.rel_path)
        if destination_folder == note_folder:
            continue
        note.move_to_folder(Path(parent_folder) / destination_folder)


# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 23
def move_information_notes_to_correct_folder_for_all_indices(
        index_of_index_notes: VaultNote, # The index note indexing other index notes; `index_of_index_notes` is intended to be an index note for an entire reference whereas the index notes are intended to correspond to chapters/sections in the reference.
        vault: PathLike,
        hints: list[PathLike] = [] # Hints on where the information notes are likely to be found at.  Each path is relative to `vault` and points to a directory.
        ) -> None:
    """
    Moves the information notes for all index notes belonging to the reference as
    specified by `index_of_index_notes`.
    """
    index_of_index_file = MarkdownFile.from_vault_note(index_of_index_notes)
    text = str(index_of_index_file)
    index_files = find_links_in_markdown_text(text)
    index_files = [ObsidianLink.from_text(text[start:end])
                   for start, end in index_files]
    for link in index_files:
        index_note_name = link.file_name
        index_note = VaultNote(vault, name=index_note_name)
        move_information_notes_to_correct_folder(
            index_note, vault, hints=hints+[index_note.rel_path])

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 33
def convert_title_to_folder_name(title: str) -> str:
    # TODO: remove left/right
    """
    Returns a folder name for the given string, e.g. replaces spaces
    with underscore.
    
    **Parameters**
    - `title` - str
    
    **Returns**
    - str
    """
    characters_to_remove = [
        '.', '\'', '$', '(', ')', '{', '}', ':', '?', '!', ',', '#', '%', '&',
        '\\', '<', '>', '*', '?', '/', '"', '@', '+', '`', '|', '=', '[', ']',
        'mathscr', 'mathbf', 'mathrm', ]
    characters_to_turn_to_underscore = [' ', '-', '^']
    title = title.strip()
    title = title.lower()
    for character in characters_to_remove:
        title = title.replace(character, '')
    for character in characters_to_turn_to_underscore:
        title = title.replace(character, '_')
    title = sanitize_filename(title)
    return title

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 35
def convert_heading_to_folder_name(
        heading: str # Matches regex `\# (\w+?)\. (.*?)`
        ) -> str:
    """Converts a heading to a valid name for a folder.
    
    TODO Might not work correctly.

    **Parameters**
    - heading: str
        
    """
    regex_match = re.match('\# (\w+?)\. (.*)', heading)
    try:
        alphanumeric = regex_match.group(1)
        title = regex_match.group(2)
    except AttributeError:
        raise ValueError(
            f"`convert_heading_to_folder_name` unsuccessfully attempted"
            f" to match the following: {heading}")
        #print(heading)
    return f'{alphanumeric}_{convert_title_to_folder_name(title)}'    

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 37
def make_folders_from_index_note_headers(
        index_note: VaultNote
        ) -> None:
    """
    Make folders in the same directory as index note whose names
    are the titles of the headers of the index note.

    The headers of the index note must match the regex pattern `\# (\w+?)\. (.*?)`.
    """
    mfile = MarkdownFile.from_vault_note(index_note)
    headings = mfile.get_headings_by_line_number(levels=1)
    pattern = re.compile('\# (\w+?)\. (.*?)')
    folder_names = [convert_heading_to_folder_name(heading)
                    for _, heading in headings.items() if heading]
    directory = Path(os.path.dirname(index_note.path()))
    for folder_name in folder_names:
        try:
            os.mkdir(directory / folder_name)
        except OSError as error:
            pass

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 40
# TODO: do an example of the `include_embedded_notes` paramtere.
def get_notes_from_index_note(
        vault: PathLike, # The path to the Obsidian vault directory
        index_note: VaultNote, # The VaultNote object for the index note.
        as_vault_notes: bool = True, # If `True`, returns the ``VaultNote`` objects for the index notes. Otherwise, returns the names of these notes 
        include_embedded_notes: bool = False # If `True`, include in the list the embedded notes. Defaults to `False`.
        ) -> list[Union[str, VaultNote]]: # Either of the names of the index notes in the vault or of the index notes as VaultNote objects, depending on `as_vault_notes`.
    """Returns the list of notes listed in the index note in the order that
    they are listed in.
    
    Asssumes that the index note is "formatted correctly".
    
    **See Also**
    - ``get_index_notes_from_index_note`` in 
    ``markdown.obsidian.personal.reference``.
    """
    vault = Path(vault)
    text = index_note.text()
    links = links_from_text(text)
    if not include_embedded_notes:
        links = [link for link in links if not link.is_embedded]
    index_notes = [link.file_name for link in links]
    if as_vault_notes:
        index_notes = [VaultNote(vault, name=index_note)
                       for index_note in index_notes]
    return index_notes

# %% ../../../../nbs/12_markdown.obsidian.personal.index_notes.ipynb 46
def add_link_in_index_note_after_note_link(
        index_note: VaultNote,
        note_to_add_link_after: VaultNote,
        link_to_add: ObsidianLink) -> None:
    """
    Adds a link in the index note.

    The link is added after the first link to `note_to_add_link_after`. 
    If no link to `note_to_add_link_after` is found, then a link is added
    at the end.
    """
    link_to_find = ObsidianLink(False, note_to_add_link_after.name, -1, -1)
    pattern = link_to_find.to_regex()
    mf = MarkdownFile.from_vault_note(index_note)
    for i, part in enumerate(mf.parts):
        if re.search(pattern, part['line']):
            break
    mf.insert_line(i + 1, {'type': MarkdownLineEnum.UNORDERED_LIST,
                           'line': f'- {link_to_add.to_string()}'})
    mf.write(index_note)
