# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright (C) 2025 The Project Authors
# See pyproject.toml for authors/maintainers.
# See LICENSE for license details.
"""
{Short module description (1-3 sentences)}
todo docstring


"""
# IMPORTS
# ***********************************************************************
# import modules from other libs

# Native imports
# =======================================================================
import glob, re
from pathlib import Path
from datetime import datetime

# ... {develop}

# External imports
# =======================================================================
import pandas as pd

# ... {develop}

# Project-level imports
# =======================================================================
from losalamos.root import MbaE, Collection
from losalamos.references import Reference, REFERENCE_DISPATCHER
from losalamos.paths import FOLDER_TEMPLATES_NOTES

# ... {develop}


# CONSTANTS
# ***********************************************************************
# define constants in uppercase

# CONSTANTS -- Project-level
# =======================================================================
# ... {develop}


# list of fields that needs harmonization
HARMONIZE_TEXT_FIELDS = [
    "name",
    "title",
    "subtitle",
    "subject",
    "abstract",
    "contract",
    "client",
    "project",
    "comment",
    "caption",
    "alttext",
    "source",
    "document",
    "file",
    "pdf",
    "file_draft",
    "publisher",
    "booktitle",
    "cite_bibli",
]
HARMONIZE_DATE_FIELDS = [
    "timestamp",
    "date",
    "datetime",
    "date_start",
    "date_end",
]
# Subsubsection example
# -----------------------------------------------------------------------

# CONSTANTS -- Module-level
# =======================================================================
# ... {develop}


# FUNCTIONS
# ***********************************************************************

# FUNCTIONS -- Project-level
# =======================================================================


def search_markdown_files(paths: list[Path]) -> list[Path]:
    """
    Recursively scans the provided paths to identify and collect all files with a ``.md`` extension.

    :param paths: A list of file or directory paths to search.
    :type paths: list[:class:`pathlib.Path`]
    :return: A list of paths pointing to the discovered markdown files.
    :rtype: list[:class:`pathlib.Path`]
    """
    files = []
    for p in paths:
        if p.is_file() and p.suffix == ".md":
            files.append(p)
        elif p.is_dir():
            files.extend(p.rglob("*.md"))
    return files


# FUNCTIONS -- Module-level
# =======================================================================
# ... {develop}


# CLASSES
# ***********************************************************************

# CLASSES -- Project-level
# =======================================================================

# NOTES
# -----------------------------------------------------------------------


class Note(MbaE):
    # todo [docstring]

    STR_HEAD = "Head"
    STR_BODY = "Body"
    STR_TAIL = "Tail"

    def __init__(self, name="MyNote", alias="Nt1"):
        # set attributes
        self.file_note = None
        self.metadata = None
        self.data = None
        super().__init__(name=name, alias=alias)
        # ... continues in downstream objects ... #

    def _set_fields(self):
        super()._set_fields()
        # Attribute fields
        self.field_name_note = "note_name"
        self.field_alias_note = "note_alias"
        self.field_file_note = "note_file"

        # Metadata fields

        # ... continues in downstream objects ... #

    def get_metadata(self):
        """
        This method returns all objects metadata, incluiding selected
        attributes beyond the metadata of the file
        """
        # set object metadata
        dc_main = {
            self.field_name_note: self.name,
            self.field_alias_note: self.alias,
            self.field_file_note: str(self.file_note),
        }

        # include file metadata
        if self.metadata is not None:
            dc_main.update(self.metadata)

        return dc_main

    def load_metadata(self, file_note=None):

        if file_note is not None:
            self.file_note = Path(file_note)

        dc = Note.parse_metadata(self.file_note)

        # DATE
        # ---------------------
        for k in dc:
            if k in HARMONIZE_TEXT_FIELDS:
                dc[k] = Note.harmonize_entry_text(dc[k])

        # DATE
        # ---------------------
        for k in dc:
            if k in HARMONIZE_DATE_FIELDS:
                dc[k] = Note.harmonize_entry_date(dc[k], key=k)

        self.metadata = dc.copy()

    def load_data(self, file_note=None):

        if file_note is not None:
            self.file_note = Path(file_note)

        self.data = Note.parse_note(self.file_note)

    def load(self, file_note=None):
        self.load_metadata(file_note=file_note)
        self.load_data(file_note=file_note)

    def save(self):
        self.to_file(file_path=self.file_note)

    def export(self, folder, filename, export_metadata=False):
        """
        Export to a markdown file with optional metadata handling.

        :param folder: The directory path where the file will be saved.
        :type folder: str
        :param filename: The name of the file without the extension.
        :type filename: str
        :param export_metadata: Toggle to trigger the parent class metadata export. Default value = ``False``
        :type export_metadata: bool
        :return: Path to the exported file
        :rtype: Path

        .. note::

            This method constructs a file path using ``pathlib.Path`` and appends the
            ``.md`` extension. It calls the internal ``to_file`` method with ``cleanup``
            enabled to finalize the document. If ``export_metadata`` is set to ``True``,
            it invokes the ``export`` method of the superclass before proceeding.

        """
        if export_metadata:
            super().export(folder=folder, filename=filename)
        fpath = Path(folder) / f"{filename}.md"
        self.to_file(fpath, cleanup=True)

        return fpath.absolute()

    def to_file(self, file_path, cleanup=True):
        """
        Write the note object's metadata and data content to a specified file.

        :param file_path: The destination path where the note will be written.
        :type file_path: str
        :param cleanup: Toggle to remove excessive blank lines after writing. Default value = ``True``
        :type cleanup: bool
        :return: None
        :rtype: NoneType

        .. note::

            This method aggregates metadata and data by calling ``metadata_to_list`` and
            ``data_to_list``. It sanitizes the output by replacing ``None`` string
            occurrences with empty strings and ensures each entry ends with a newline
            character. If ``cleanup`` is enabled, it post-processes the file using
            ``remove_excessive_blank_lines`` to maintain consistent formatting.

        """
        # metadata
        # ------------------------------------
        ls_metadata = Note.metadata_to_list(self.metadata)

        for i in range(len(ls_metadata)):
            # clear "None" values
            ls_metadata[i] = ls_metadata[i].replace("None", "")

        # data
        # ------------------------------------
        ls_data = Note.data_to_list(self.data)
        # append to metadata list
        for l in ls_data:
            ls_metadata.append(l[:])
        ls_all = [line + "\n" for line in ls_metadata]

        with open(file_path, "w", encoding="utf-8") as file:
            file.writelines(ls_all)

        # clean up excessive lines
        if cleanup:
            Note.remove_excessive_blank_lines(file_path)

        return None

    def refactor(self, new_name, scope):
        """
        Renames the current file note and updates all internal markdown
        links within a specified scope.

        .. note::

             This function performs a regex-based search across the provided scope to find and replace
             Obsidian-style internal links (e.g., ``[[OldName]]`` or ``[[OldName|Alias]]``) with the
             newly defined name.

        :param new_name: The new filename (without extension) to be applied.
        :type new_name: str
        :param scope: The directories or file paths where link references should be updated.
        :type scope: :class:`pathlib.Path`, str, or Iterable
        :return: None
        :rtype: None
        """
        from typing import Iterable, Union

        def _normalize_scope(scope):
            if scope is None:
                return [source]  # only self

            if isinstance(scope, (str, Path)):
                return [Path(scope)]

            if isinstance(scope, Iterable):
                return [Path(p) for p in scope]

            raise ValueError("Invalid scope for refactoring")

        source = Path(self.file_note)
        old_name = source.stem

        if old_name == new_name:
            return None

        # Rename file itself
        new_path = source.with_name(f"{new_name}.md")
        source.rename(new_path)
        self.file_note = new_path

        # determine scope
        scope_paths = _normalize_scope(scope)

        # search files
        md_files = search_markdown_files(scope_paths)

        # Build regex
        pattern = re.compile(rf"\[\[{re.escape(old_name)}(\|[^\]]+)?\]\]")

        # Replace in files
        for file in md_files:
            text = file.read_text(encoding="utf-8")

            new_text = pattern.sub(rf"[[{new_name}\1]]", text)

            if new_text != text:
                file.write_text(new_text, encoding="utf-8")

        return None

    @staticmethod
    def harmonize_entry_text(entry):
        if entry is not None:
            new_entry_bulk = entry[:]
            new_entry_bulk = new_entry_bulk.replace("'", "`")
            new_entry_bulk = new_entry_bulk.replace('"', "``")

            new_entry = '"' + new_entry_bulk + '"'
            s_start = new_entry[:2].replace("`", "")
            s_end = new_entry[-2:].replace("`", "")
            s_bulk = new_entry[2:-2]

            return s_start + s_bulk + s_end
        else:
            return None

    @staticmethod
    def harmonize_entry_date(entry, key="date"):
        if entry is None:
            return None

        if "datetime" in key or "timestamp" in key:
            format_mask = "%Y-%m-%d %H:%M:%S"
        else:
            format_mask = "%Y-%m-%d"

        dt = pd.to_datetime(entry, errors="coerce")

        if pd.isna(dt):
            return None

        return dt.strftime(format_mask)

    @staticmethod
    def harmonize_entry_date_old(entry, key="date"):
        if entry is not None:

            format_mask = "%Y-%m-%d"

            if "datetime" in key:
                format_mask = "%Y-%m-%d %H:%M:%S"
            if "timestamp" in key:
                format_mask = "%Y-%m-%d %H:%M:%S"

            # Convert string back into a Python object
            dt_object = pd.to_datetime([entry])

            # return string
            return str(dt_object.strftime(format_mask).values[0])
        else:
            return None

    @staticmethod
    def remove_excessive_blank_lines(file_path):
        """
        Remove consecutive blank lines from a file to ensure only single blank lines remain.

        :param file_path: The path to the target text file to be processed.
        :type file_path: str
        :return: None
        :rtype: NoneType

        .. note::

            This method performs an in-place modification of the file. It iterates through
            the content and suppresses any sequence of empty lines that exceeds a
            single occurrence, effectively "squeezing" the vertical whitespace.

        """
        with open(file_path, "r", encoding="utf-8") as file:
            lines = file.readlines()

        cleaned_lines = []
        previous_line_blank = False

        for line in lines:
            if line.strip() == "":
                if not previous_line_blank:
                    cleaned_lines.append(line)
                    previous_line_blank = True
            else:
                cleaned_lines.append(line)
                previous_line_blank = False

        with open(file_path, "w", encoding="utf-8") as file:
            file.writelines(cleaned_lines)

    @staticmethod
    def parse_metadata(note_file):
        """
        Extracts YAML metadata from the header of a Markdown file.

        :param note_file: str, path to the Markdown file
        :return: dict, extracted YAML metadata
        """
        with open(note_file, "r", encoding="utf-8") as file:
            content = file.read()

        # Regular expression to match the YAML header
        yaml_header_regex = r"^---\s*\n(.*?)\n---\s*\n"

        # Search for the YAML header in the content
        match = re.search(yaml_header_regex, content, re.DOTALL)

        if match:
            yaml_content = match.group(1)
            return Note.parse_yaml(yaml_content)
        else:
            return None

    @staticmethod
    def parse_yaml(yaml_content):
        """
        Parses YAML content into a dictionary.

        :param yaml_content: str, YAML content as string
        :return: dict,  parsed YAML content
        """
        metadata = {}
        lines = yaml_content.split("\n")
        current_key = None
        current_list = None

        for line in lines:
            if line.strip() == "":
                continue
            if ":" in line:
                key, value = line.split(":", 1)
                key = key.strip()
                value = value.strip()
                if value == "":  # start of a list
                    current_key = key
                    current_list = []
                    metadata[current_key] = current_list
                else:
                    if key == "tags":
                        metadata[key] = [
                            v.strip() for v in value.split("-") if v.strip()
                        ]
                    else:
                        metadata[key] = value
            elif current_list is not None and line.strip().startswith("-"):
                current_list.append(line.strip()[1:].strip())

        # fix empty lists
        for e in metadata:
            if len(metadata[e]) == 0:
                metadata[e] = None

        # fix text fields
        for e in metadata:
            if metadata[e]:
                size = len(metadata[e]) - 1
                if metadata[e][0] == '"' and metadata[e][size] == '"':
                    # slice it
                    metadata[e] = metadata[e][1:size]

        return metadata

    @staticmethod
    def metadata_to_list(metadata_dict):
        """
        Convert a dictionary of metadata into a formatted list of strings.

        :param metadata_dict: A dictionary containing metadata keys and values to be formatted.
        :type metadata_dict: dict
        :return: A list of strings formatted with YAML-like syntax, enclosed by dashed separators.
        :rtype: list

        .. note::

            The method processes dictionary entries into a human-readable list format. It
            handles list values by creating indented bullet points and converts ``None``
            values into empty strings. The resulting list starts and ends with a ``---``
            delimiter string.

        """
        ls_metadata = []
        ls_metadata.append("---")
        for e in metadata_dict:
            if isinstance(metadata_dict[e], list):
                ls_metadata.append("{}:".format(e))
                for i in metadata_dict[e]:
                    ls_metadata.append(" - {}".format(i))
            else:
                aux0 = metadata_dict[e]
                if aux0 is None:
                    aux0 = ""
                aux1 = "{}: {}".format(e, aux0)
                ls_metadata.append(aux1)
        ls_metadata.append("---")

        return ls_metadata

    @staticmethod
    def data_to_list(data_dict):
        """
        Flatten a dictionary of lists into a single list separated by blank lines and delimiters.

        :param data_dict: A dictionary where each key maps to a list of strings to be aggregated.
        :type data_dict: dict
        :return: A concatenated list of all values with added structural spacing and separators.
        :rtype: list

        .. note::

            This function iterates through the top-level keys of ``data_dict`` and appends
            the contents of each list to a master list. After each group of data, it
            inserts an empty string, a ``---`` separator, and another empty string to
            visually distinguish different levels or sections.

        """
        ls_out = []
        for level in data_dict:
            ls_out = ls_out + data_dict[level][:]
            if level != Note.STR_TAIL:
                ls_out.append("")
                ls_out.append("---")
        return ls_out

    @staticmethod
    def parse_note(file_path):
        """
        Extract and categorize note content into head, body, and tail sections based on separators.

        :param file_path: The path to the note file to be parsed.
        :type file_path: str
        :return: A dictionary containing the cleaned lines for ``Head``, ``Body``, and ``Tail``.
        :rtype: dict

        .. note::

            The function first identifies and skips an initial YAML frontmatter block if it
            starts with ``---``. It then uses the ``---`` string as a delimiter to
            segment the remaining text. If multiple separators exist, the first and last
            act as boundaries for the ``Body``, while everything before the first is
            ``Head`` and everything after the last is ``Tail``. All extracted lines
            undergo a ``strip()`` operation to remove leading/trailing whitespace.


        """
        with open(file_path, "r", encoding="utf-8") as file:
            lines = file.readlines()

        # Skip YAML header if present
        if lines[0].strip() == "---":
            yaml_end_index = lines.index("---\n", 1) + 1
            lines = lines[yaml_end_index:]

        # Find all separator positions (lines with "---")
        separator_indices = [i for i, line in enumerate(lines) if line.strip() == "---"]

        # Default values for Head, Body, and Tail
        head, body, tail = [], [], []

        if len(separator_indices) == 0:
            # No separators, the whole content is the Body
            body = lines
        elif len(separator_indices) == 1:
            # One separator: Head is before, Body is between, Tail is after
            head = lines[: separator_indices[0]]
            body = lines[separator_indices[0] + 1 :]
        elif len(separator_indices) == 2:
            # Two separators: Head, Body, and Tail
            head = lines[: separator_indices[0]]
            body = lines[separator_indices[0] + 1 : separator_indices[1]]
            tail = lines[separator_indices[1] + 1 :]
        else:
            # More than two separators: Head is before the first, Body is between the first and last, Tail is after the last
            head = lines[: separator_indices[0]]
            body = lines[separator_indices[0] + 1 : separator_indices[-1]]
            tail = lines[separator_indices[-1] + 1 :]

        # Clean up any extra newlines from the content
        head = [line.strip() for line in head]
        body = [line.strip() for line in body]
        tail = [line.strip() for line in tail]

        return {Note.STR_HEAD: head, Note.STR_BODY: body, Note.STR_TAIL: tail}

    @staticmethod
    def list_by_pattern(md_dict, patt_type="tag"):
        """
        Retrieve a list of patterns from the note dictionary.

        :param md_dict: Dictionary containing note sections.
        :type md_dict: dict
        :param patt_type: Type of pattern to search for, either "tag" or "related". Defaults to "tag".
        :type patt_type: str
        :return: List of found patterns or None if no patterns are found.
        :rtype: list or None
        """

        if patt_type == "tag":
            pattern = re.compile(r"#\w+")
        elif patt_type == "related":
            pattern = re.compile(r"\[\[.*?\]\]")
        else:
            pattern = re.compile(r"#\w+")

        patts = []
        # run over all sections
        for s in md_dict:
            content = md_dict[s]["Content"]
            for line in content:
                patts.extend(pattern.findall(line))

        if len(patts) == 0:
            patts = None

        return patts


class NoteBasic(Note):

    TEMPLATE_FILE = FOLDER_TEMPLATES_NOTES / "_basic.md"
    THUMBNAIL_SIZE = None
    PATTERN_ABSTRACT = "[!Abstract]"

    def __init__(self, name="MyNote", alias="Nt1"):
        super().__init__(name=name, alias=alias)
        self.file_note_template = self.get_template_file()
        self.metadata_standard = self.load_metadata_standard()
        self.data_standard = self.load_data_standard()

    @classmethod
    def get_template_file(cls):
        """
        Retrieves the filesystem path of the template file associated with the class.

        :return: The path to the template file.
        :rtype: :class:`pathlib.Path`
        """
        return Path(cls.TEMPLATE_FILE)

    def load_new(self, file_note):
        """
        Initializes a new note instance using a template and assigns it a new file path.

        :param file_note: The destination path where the new note will be saved.
        :type file_note: str or :class:`pathlib.Path`
        """
        self.file_note = self.file_note_template
        self.load()
        self.file_note = Path(file_note)
        self.update()
        self.update_timestamp()

    def load_data_standard(self):
        """
        Parses and returns the standard data content from the template file.

        :return: A dictionary containing the parsed data from the template.
        :rtype: dict
        """
        dc = Note.parse_note(file_path=self.file_note_template)
        return dc

    def load_metadata_standard(self):
        """
        Retrieves the metadata structure from the template and initializes all values to ``None``.

        :return: A dictionary of metadata keys with cleared values.
        :rtype: dict
        """
        dc = Note.parse_metadata(note_file=self.file_note_template)
        for k in dc:
            dc[k] = None
        return dc

    def load_metadata(self, file_note=None):
        """
        Loads metadata and synchronizes it against the standard template schema.

        .. important::

            This method filters the current metadata to ensure only keys present in
            ``metadata_standard`` are kept, filling missing keys with ``None``.

        :return: No value is returned.
        :rtype: None
        """
        super().load_metadata(file_note=file_note)
        # filter standard entries
        dc = {}
        for k in self.metadata_standard:
            # filter standard entry
            if k in self.metadata:
                dc[k] = self.metadata[k]
            # add standard entry
            else:
                dc[k] = None

        self.metadata = dc.copy()
        return None

    def reset_data(self):
        """
        Resets all data segments including ``Head``, ``Body`` and ``Tail`` to their standard values.

        .. danger::

            This action will erase all current data in the instance.

        """
        self.reset_data_head()
        self.reset_data_body()
        self.reset_data_tail()

    def reset_data_segment(self, segment):
        """
        Resets a specific data segment to its standard default state.

        :param segment: The name of the data segment to reset (e.g., ``Head``, ``Body``, or ``Tail``).
        :type segment: str

        .. danger::

            This action will erase the current data for the specified segment.

        """
        dc = self.load_data_standard()
        self.data[segment] = dc[segment][:]
        ### self.update()

    def reset_data_head(self):
        """
        Resets the ``Head`` data segment to its standard default state.

        .. danger::

            This action will erase the current data segment.

        """
        self.reset_data_segment(self.STR_HEAD)

    def reset_data_body(self):
        """
        Resets the ``Body`` data segment to its standard default state.

        .. danger::

            This action will erase the current data segment.

        """
        self.reset_data_segment(self.STR_BODY)

    def reset_data_tail(self):
        """
        Resets the ``Tail`` data segment to its standard default state.

        .. danger::

            This action will erase the current data segment.

        """
        self.reset_data_segment(self.STR_TAIL)

    def refactor(self, new_name, scope):
        """
        Performs a full refactor operation by renaming the resource and
        refreshing its local data state.

        :param new_name: The new filename to be assigned to the resource.
        :type new_name: str
        :param scope: The scope of files where references to this resource should be updated.
        :type scope: :class:`pathlib.Path`, str, or Iterable
        :return: None
        :rtype: None
        """
        super().refactor(new_name=new_name, scope=scope)
        self.load()
        self.update()
        self.save()

    def update(self):
        """
        Triggers a sequence of internal updates to synchronize the note's name, abstract, and thumbnail.

        .. note::

            This method acts as a base update sequence; additional update
            logic may be implemented in downstream classes.

        """
        self.update_name()
        self.update_abstract()
        self.update_thumbnail()
        # -- continues in downstream classes

    def update_name(self):
        """
        Updates the note metadata and the first line of the Head segment with the current file stem.

        .. warning::

            This method assumes the first item in the ``Head`` data segment is the title and will overwrite it.

        """
        current_name = Path(self.file_note).stem
        self.metadata["name"] = current_name
        # always the first item
        self.data[self.STR_HEAD][0] = f"# {current_name}"

    def update_abstract(self):
        """
        Synchronizes the abstract from metadata into the Head data segment block.

        .. note::

            The method searches for the ``self.PATTERN_ABSTRACT`` identifier within the ``Head``
            segment and replaces the subsequent line with the formatted abstract string.

        """
        if self.metadata["abstract"] is not None:
            current_abstract = self.metadata["abstract"][1:-1]
            n = 0
            for line in self.data[self.STR_HEAD]:
                n = n + 1
                if self.PATTERN_ABSTRACT in line:
                    break
            if n > 0:
                self.data[self.STR_HEAD][n] = f"> {current_abstract}\n"

    def update_thumbnail(self, image_name=None):
        """
        Updates the thumbnail image link in the Head data segment based
        on the current file name and predefined size.

        .. note::

            The method searches for an existing Wikilink image pattern
            (``![[``) within the ``Head`` segment to perform the replacement.

        """
        size = self.THUMBNAIL_SIZE

        if image_name is None:
            image_name = self.file_note.stem

        if size is not None:
            n = 0
            for line in self.data[self.STR_HEAD]:
                if "![[" in line[:3]:
                    break
                n = n + 1
            if n > 0:
                self.data[self.STR_HEAD][n] = f"![[{image_name}.jpeg|{size}]]"

    def update_timestamp(self):
        """
        Records the current local date and time into the note metadata.

        .. note::

            The timestamp is formatted as a string following
            the ``%Y-%m-%d %H:%M:%S`` pattern.

        """
        from datetime import datetime

        now = datetime.now()
        self.metadata["timestamp"] = now.strftime("%Y-%m-%d %H:%M:%S")

    def update_note(self, file_note=None):
        if file_note is not None:
            self.load(file_note=file_note)
        self.update()
        self.save()


class NoteProject(NoteBasic):

    TEMPLATE_FILE = FOLDER_TEMPLATES_NOTES / "_project.md"
    THUMBNAIL_SIZE = None


class NoteJournal(NoteBasic):

    TEMPLATE_FILE = FOLDER_TEMPLATES_NOTES / "_journal.md"
    THUMBNAIL_SIZE = 300

    def update(self):
        super().update()
        self.update_title()

    def update_title(self):
        name = self.metadata["name"]
        self.metadata["title"] = f'"{name}"'


class NoteFigure(NoteBasic):

    TEMPLATE_FILE = FOLDER_TEMPLATES_NOTES / "_figure.md"
    THUMBNAIL_SIZE = 500


class NoteReference(NoteBasic):

    TEMPLATE_FILE = FOLDER_TEMPLATES_NOTES / "_reference.md"
    THUMBNAIL_SIZE = 200
    PATTERN_ABSTRACT = "[!Info]"

    def __init__(self):
        super().__init__()
        self.cite_style = "apa"

    def get_reference(self):
        # todo docstring
        dc = self.metadata.copy()
        ref = Reference.get_by_entry(entry_type=dc["entry_type"])
        ref.data = dc.copy()
        ref.standardize()
        return ref

    def get_str_bib(self, drop_fields=None):
        # todo docstring
        ref = self.get_reference()

        if drop_fields:
            for drop in drop_fields:
                del ref.data[drop]

        return ref.get_str_bib()

    def update(self):
        # todo docstring
        self.update_name()
        self.update_cite_inline()
        self.update_cite_bibli()
        self.update_pdf()

        self.update_data_head()
        self.update_data_tail()

        self.update_abstract()
        self.update_thumbnail()

    def update_cite_inline(self):
        # todo docstring
        self.metadata["cite_inline"] = Reference.cite_line(
            bib_dict=self.metadata, text_format="plain", embed_link=False
        )

    def update_cite_bibli(self):
        # todo docstring
        s = Reference.cite_bibli(
            bib_dict=self.metadata, text_format="plain", style=self.cite_style
        )
        self.metadata["cite_bibli"] = f'"{s}"'

    def update_pdf(self):
        # todo docstring
        self.metadata["pdf"] = '"[[{}.pdf]]"'.format(self.metadata["name"])

    def update_data_head(self):
        # todo docstring
        self.reset_data_head()

        self.update_name()

        # Handle article case
        if self.metadata["entry_type"] == "article":
            journal = self.metadata["journal"]
            if journal is None:
                pass
            else:
                old_line = "By `=this.cite_inline`"
                new_line = "By `=this.cite_inline` in [[{}]]".format(journal)
                c = 0
                for line in self.data[self.STR_HEAD]:
                    if old_line in line:
                        self.data[self.STR_HEAD][c] = line.replace(old_line, new_line)
                    c = c + 1

    def update_data_tail(self):
        # todo docstring
        self.reset_data_tail()

        # replace inline and bibli
        dc = self.metadata.copy()

        ls_drop = ["pdf", "abstract"]
        dc["bibtex"] = self.get_str_bib(drop_fields=ls_drop)

        ls = []
        for line in self.data[self.STR_TAIL]:
            line = line.format(**dc).strip('"')
            ls.append(line[:])
        self.data[self.STR_TAIL] = ls[:]

    def update_thumbnail(self, image_name=None):
        # todo docstring
        if self.metadata["entry_type"] == "article":
            image_name = self.metadata["journal"]
        super().update_thumbnail(image_name=image_name)

    def to_bib(self, output, drop_pdf=True):
        # todo docstring
        ref = self.get_reference()
        if drop_pdf:
            del ref.data["pdf"]
        ref.to_bib(output=output)


# COLLECTIONS
# -----------------------------------------------------------------------


class NoteCollection(Collection):

    # todo docstring

    BASE_OBJECT = Note

    def __init__(self, name="MyNoteColl", alias="NtCol0"):

        super().__init__(base_object=self.BASE_OBJECT, name=name, alias=alias)

    def load_list(self, files):
        """
        Iterates through a list of file paths to initialize, load, and append objects to the collection.

        :param files: A list of file paths to be processed.
        :type files: list
        :return: None
        :rtype: None
        """
        for f in files:

            p = Path(f)
            name = p.stem

            n = self.baseobject(name=name, alias=name)
            n.file_note = p
            n.load()

            self.append(n)
        return None

    def load_folder(self, folder):
        """
        Identifies all Markdown files within a specific directory and adds them to the collection.

        :param folder: The directory path to scan for ``.md`` files.
        :type folder: str
        :return: None
        :rtype: None
        """
        ls = glob.glob(str(Path(folder) / "*.md"))

        self.load_list(files=ls)

        return None

    def load_pattern(self, pattern):
        """
        Uses a glob pattern to locate files and load them into the collection.

        :param pattern: The search pattern (e.g., ``path/to/*/*.md``) used to match files.
        :type pattern: str
        :return: None
        :rtype: None

        """
        ls = glob.glob(pattern)
        self.load_list(files=ls)
        return None


class NoteCollBasic(NoteCollection):

    BASE_OBJECT = NoteBasic


class NoteCollProject(NoteCollection):

    BASE_OBJECT = NoteProject


class NoteCollJournal(NoteCollection):

    BASE_OBJECT = NoteJournal


class NoteCollFigure(NoteCollection):

    BASE_OBJECT = NoteFigure


# ... {develop}

# CLASSES -- Module-level
# =======================================================================
# ... {develop}


# SCRIPT
# ***********************************************************************
# standalone behaviour as a script
if __name__ == "__main__":
    # Script section
    # ===================================================================
    print("Hello world!")
    # ... {develop}

    # Script subsection
    # -------------------------------------------------------------------
    # ... {develop}
