# SPDX-FileCopyrightText: © 2023 Christian BUHTZ <c.buhtz@posteo.jp>
# SPDX-FileCopyrightText: © 2025 Michael Olie <aolie1794@gmail.com>
#
# SPDX-License-Identifier: GPL-3.0-only
#
# This file is part of the program "Hyperorg" which is released under
# GNU General Public License v3 (GPLv3). See LICENSES directory
# or go to <https://spdx.org/licenses/GPL-3.0-only.html>.

"""The content module offering the Node class."""
from __future__ import annotations
import logging
from collections import defaultdict
from collections.abc import Hashable
from typing import Iterable, Dict
from hyperorg import elements
from hyperorg import textruns
from hyperorg import parser

_log = logging.getLogger(__name__)


class Node:
    """Org-file body in a generalized and strcutured format.

    The body of an org-file with all its elements (paragraphs, lists, ...) and
    their structure is represented by an object of that clase. On base of one
    of its objects it can easiely transformed to another file-format
    (e.g. HTML). An object Content is the input for the Exporter class.

    The Org syntax reference can be found at
    https://orgmode.org/worg/dev/org-syntax.html. It is not an official
    document but there is not a more official. I was pointed to that document
    asking on the mailinglist.

    Development note:
        A node (with "d") in OrgRoam is an entity with an ID. A note (with
        "t") is something more broad.

        Escaping HTML entities (e.g. "&" into "&amp;") is done here in that
        class (see Issue #47). This is not a clear solution because HTML
        related operations should be done in `Exporter`. The `Content` class
        should be independent from output format. In consequence the current
        solution is not for ever and will be refactored once.
    """

    input_dir = None
    """Input directory where the Org-roam content was read from"""

    nodes: dict = {}
    """List of all Org-roam nodes in `Content` format"""

    heading_ids: dict = {}
    """Org-IDs of headings (subtrees)"""

    IDX_ALL = 'all'

    IDX_FILETAG = 'filetag'

    IDX_NO_TAG = '(no tags)'

    index_data: dict = {
        IDX_ALL: defaultdict(list),
        IDX_FILETAG: defaultdict(lambda: defaultdict(list))
    }
    """Nested dictionary containing glossary like data."""

    META_FIELDS_INHERIT = ['date', 'filetags']
    """See `inherit_meta_fields_from()`. """

    __slots__ = (
        '_orgids_linking',
        'filename',
        '_meta',
        'title_org',
        'title_runs',
        '_elements',
        # '_e',
    )

    def __init__(self,
                 body_lines: Iterable[str],
                 filename: str,
                 title_aliases: Iterable[str] = None,
                 anchor_first_heading: bool = False):
        """Content ctor."""

        if title_aliases is None:
            title_aliases = []

        self._orgids_linking = []

        # Name of the original Org-roam file
        self.filename = filename

        # Node title
        self.title_org = '[no title]'
        self.title_runs = textruns.cut_into_runs(self.title_org)

        # Meta data about that Org-roam node
        self._meta = {}

        # Title aliases (from :ROAM_ALIASES)
        if title_aliases:
            self._meta['aliases_org'] = []
            self._meta['aliases_runs'] = []
            for one_alias in title_aliases:
                self._meta['aliases_org'].append(one_alias)
                self._meta['aliases_runs'].append(
                    textruns.cut_into_runs(one_alias))

        # list of content elements
        self._elements = []

        # # the current active element
        # self._e = None

        self.parse_lines(body_lines)

        # When this Content instance represent a subheading with its own OrgID
        if anchor_first_heading:
            if not isinstance(self._elements[0], elements.Heading):
                raise TypeError('First element need to be a heading.')

            self._elements[0].generate_anchor()

    def _on_link_created(self, linkrun):
        if linkrun.link_type == textruns.Link.Type.ORGID:
            self._orgids_linking.append(linkrun.target)

    def __getitem__(self, idx: int):
        return self._elements[idx]

    def __len__(self):
        return len(self._elements)

    def add_subheading_content(self, content):
        """Add child node to this one.
        """

        content.inherit_meta_fields_from(self)

        # Content element, subtree/heading with own OrgId
        # self._finish_current_element()
        # self._e = content
        self._elements.append(content)
        # self._finish_current_element()

    def inherit_meta_fields_from(self, content: Node):
        """Some meta fields from ``content`` are copied into the own.

        Which fields are copied is specified by `META_FIELDS_INHERIT'.

        Args:
            content: The content object to copy from.
        """

        for field in Node.META_FIELDS_INHERIT:
            try:
                val = content._meta[field]  # pylint: disable=protected-access
            except KeyError:
                pass
            else:
                own_val = self._meta.get(field, None)

                # just copy
                if own_val is None:
                    self._meta[field] = val
                    continue

                if not isinstance(own_val, list):
                    own_val = [own_val]

                if not isinstance(val, list):
                    val = [val]

                self._meta[field].extend(val)

    @classmethod
    def add_all_backlinks(cls,
                          backlinks: Dict[Hashable, Dict[Hashable, Iterable]]):
        """Create backlinks elements for all nodes.

        The backlinks dict contains backlinks of all existing nodes like this
        example ::

            {
                'node-A': {
                    'node-B': [],
                    'node-C': ['sub-C'],
                },
                'other-node': {
                    'node-X': [],
                }
            }

        "node-A" with two backlinks. One to "node-B" and one to the
        sub-heading "sub-C" of "node-C".
        "other-node" with one backlink to "node-X".

        """

        for orgid, to_backlink_node in backlinks.items():
            if orgid in cls.nodes:
                cls.nodes[orgid].add_backlinks(to_backlink_node)

    def as_link(self, own_orgid: str, subheading_idx: int = None
                ) -> textruns.Link:
        """Itself as a `Link`.

        Dev note (2025-03, buhtz): Nodes don't know their own OrgID. I am
        not sure anymore if this was such a good idea to keep the nodes that
        dumb.
        """
        if subheading_idx is None:
            runs = self.title_runs
        else:
            runs = self[subheading_idx][0].runs

        return textruns.Link(
            target=own_orgid,
            link_type=textruns.Link.Type.ORGID,
            label_runs=runs
        )

    def add_backlinks(self, backlinks: Dict[Hashable, Iterable]):
        """Create backlink element for a specific node.
        """

        for orgid, to_backlink_node in backlinks.items():
            # Create backlink element if there is not one
            if (not self._elements
                    or not isinstance(self._elements[-1], elements.Backlinks)):
                self._elements.append(
                    elements.Backlinks())

            # create org-file line
            link = Node.nodes[orgid].as_link(orgid)

            # add the link
            self._elements[-1].add_link(link)

            # subheadings?
            for subheading_id in to_backlink_node:
                idx = Node.heading_ids[subheading_id][1]
                subheading_link = Node.nodes[orgid].as_link(subheading_id, idx)
                self._elements[-1].add_sub_link(subheading_link)

    def parse_lines(self, to_parse: list):
        """Create Content elements based on text lines.

        Classify a list of lines and create Content elements
        (e.g. Content.Paragraph) of it.
        To differentiate a list item from a head when an asterix is used as
        bullet it is important to check for heading first.
        """
        with textruns.Link.event_link_created.register_temp(
                self._on_link_created):

            meta, elem = parser.OrgContentParser(to_parse).result()

            self._meta = {**self._meta, **meta}
            self._elements.extend(elem)

            try:
                # Title as runs is used in the <h1> element.
                # But when handling (back)links it might be easier using the
                # original org-syntax not parsed into runs.
                self.title_org = self._meta.pop('title')
                self.title_runs = textruns.cut_into_runs(self.title_org)
            except KeyError:
                pass

    def get_orgid_links(self) -> list:
        """Give a list of OrgID's this content instance is linking to.

        Be aware that subheading content objects are ignored.

        Returns:
            List of OrgID's
        """

        return self._orgids_linking
