"""Module implementing the page class which represents a page to be parsed.

The page object can parse the contents of the loaded file, handle the logic
for linking references to url's, and can write the pages to the destination.

"""

import os
import docutils.core

from docutils import nodes
from types import SimpleNamespace
from quaker_lib.util import make_id
from quaker_lib.directives.sphinx import ref_role
from quaker_lib.directives.metadata import get_metadata


class Page:
    """Handler for a single page.

    Responsible for parsing and writing files as well as keeping track of any
    unresolved references so that it knows when writing is possible.

    Parameters
    ----------
    main : Main
        The instance of main, from which settings are extracted.
    path : pathlib.Path
        The path to the file to be handled.

    Attributes
    ----------
    main : Main
        Store the instance of main.
    path : pathlib.Path
        Store the path to be handled.
    path_html : str
        The path with the html suffix as a string.
    src : pathlib.Path
        The path to the file from the source folder ('absolute' for us).
    dest : pathlib.Path
        The path to the file from the destination folder ('absolute' for us).
    doctree
        The AST generated by docutils.
    unresolved_references : int
        The number of unresolved references, used to check when to write.

    """

    def __init__(self, main, path):
        self.main = main
        self.path = path
        self.path_html = str(path.with_suffix('.html'))
        self.src = main.source_path / path
        self.dest = main.dest_path / self.path_html
        self.doctree = None
        self.unresolved_references = 0

    def parse(self):
        """ Parse an RST file. """

        # Add prologue and epilogue to source file.
        content = (f"{self.main.conf_vars.get('rst_prolog', '')}\n"
                   f"{self.src.read_text()}\n"
                   f"{self.main.conf_vars.get('rst_epilog', '')}")

        # Give the reference role access to the current page.
        ref_role.page = self

        self.main.docutil_settings['env'].docname = self.src.name

        # Parse the file contents.
        self.doctree = docutils.core.publish_doctree(
            content,
            parser=self.main.source_parsers.get(self.src.suffix)(),
            source_path=str(self.src),
            settings_overrides={
                **self.main.docutil_settings,
                'page': self,
                'main': self.main,
            }
        )

        # Find the page title.
        try:
            self.title = next(iter(self.doctree.traverse(nodes.title)))[0]
            self.title = self.title.astext()
        except StopIteration:
            self.title = ''

        self.handle_references()

        # Write if all the references are already resolved.
        if self.unresolved_references == 0:
            self.write()

    def use_reference(self, ref: str) -> str:
        """ Handle the logic for safely linking references.

        Paramaters
        ----------
        ref : str
            The reference to be linked.

        Returns
        -------
        ref : str
            The potentially altered reference.

        """
        # If the reference is an off site url keep it.
        if (ref.count('.') >= 2
                or ref.startswith('https://')
                or ref.startswith('http://')):
            return ref

        # If absolute path simply add to waiting list.
        if ref[0] == '/':
            pass
        # If it starts with a # handle it as a local section.
        elif ref[0] == '#':
            ref = '/' + str(self.path) + ref
        # If relative path prepend the current path.
        elif '/' in ref or '.' in ref or '#' in ref:
            ref = '/' + str(self.path.parent / ref)
        # Otherwise assume (global) id (else user should prepend ./).
        else:
            pass

        ref = make_id(ref)

        # Update the unresolved references if it cannot be linked yet.
        if ref not in self.main.id_map:
            self.main.waiting[ref].append(self)
            self.unresolved_references += 1

        return ref

    def id_to_map(self, id: str, node, anchor: str = ''):
        """Helper method to add a new id to the id_map.

        Parameters
        ----------
        id : str
            The id to be added.
        node
            The node the id maps to.
        anchor : str
            The potential section anchor to append.

        Returns
        -------
        references : list
            List of the references that were added to the map.

        """
        references = [id, '/' + str(self.path.with_suffix('')) + anchor]

        # Create the linking content object.
        contents = SimpleNamespace()
        contents.url = self.path_html + anchor
        contents.title = None
        contents.sections = []  # We will fill these later.

        # Get the title.
        title_node = node.next_node(nodes.Titular)
        if title_node:
            contents.title = title_node.astext()

        # Add all references.
        for ref in references:
            self.main.id_map[ref] = contents

        return references

    def handle_references(self):
        """ Take care of the references and linkage. """

        # Add the file name as an id as well.
        page_id = make_id(str(self.path.name))

        # Add all the ids to the reference map.
        all_references = self.id_to_map(page_id, self.doctree)

        for id, node in self.doctree.ids.items():
            all_references += self.id_to_map(id, node, anchor='#' + id)

        # Loop over the sections of this node, and add these to the
        # contents of this page's reference.
        stack = [self.doctree]
        first = True
        while stack:
            node = stack.pop()

            # Skip the node if no ids were found.
            if not node.attributes['ids']:
                continue

            id = node.attributes['ids'][0]
            sections = self.main.id_map[id].sections

            if first:
                first = False
                self.main.id_map[all_references[1]].sections = sections

            for id in node.attributes['ids'][1:]:
                self.main.id_map[id].sections = sections

            # Fill the sections:
            for child in node.children:
                if not isinstance(child, nodes.section):
                    continue

                id = child.attributes['ids'][0]
                id = '/' + make_id(str(self.path) + '#' + id)
                sections.append(id)

                # Go over the subsections.
                if len(child.children) > 0:
                    stack.append(child)

        # Resolve all the references we have added.
        for ref in all_references:
            try:
                for page in self.main.waiting.pop(ref):
                    page.unresolved_references -= 1
                    if page.unresolved_references == 0:
                        page.write()
            except KeyError:
                pass

    def get_settings_overrides(self):
        """Get the settings_override for this page.
        Used as arg for the call to docutils.core.publish_from_doctree
        in write.

        Returns
        -------
        dict
            The argument expected by publish_from_doctree.
        """
        return {
            'toc': self.main.toc_navigation,
            'template': self.main.theme.get_template(),
            'stylesheet': os.path.join(
                '_static',
                self.main.conf_vars.get('html_style',
                                        self.main.theme.get_style())),
            'src_dir': self.main.source_path,
            'dest_dir': self.main.relative_path(self.src),
            'html_path': self.path_html,
            'embed_stylesheet': False,
            'rel_base': os.path.relpath(self.main.dest_path,
                                        self.dest.parent),
            'handlers': self.main.sp_app.get_handlers(),
            'favicon': self.main.conf_vars.get('html_favicon', None),
            'logo': self.main.conf_vars.get('html_logo', None),
            'copyright': self.main.conf_vars.get('copyright', ''),
            'id_map': self.main.id_map
        }

    def del_skipped_nodes(self):
        """Delete the nodes from the page's doctree that match SKIP_TAGS."""
        for node in self.doctree.traverse():
            for i, child in reversed(list(enumerate(node.children))):
                if child.tagname in self.main.SKIP_TAGS:
                    del node[i]

    def write(self):
        """
        Parse an RST file and write its contents to a file.
        """

        # Get the page metadata.
        metadata = get_metadata(self.doctree)
        if metadata.ignore:
            return

        # Delete the nodes we want to skip.
        self.del_skipped_nodes()

        # Collect all the text content to add the page to the index.
        content = ' '.join(n.astext()
                           for n in self.doctree.traverse(nodes.Text))
        self.main.idx.add_file(content, self.title,
                               self.path_html, metadata.priority)

        # Create the output file contents.
        output = docutils.core.publish_from_doctree(
            self.doctree,
            destination_path=self.dest,
            writer=self.main.writer(),
            settings_overrides=self.get_settings_overrides())

        # Write the document to a file.
        self.dest.parent.mkdir(parents=True, exist_ok=True)
        self.dest.write_bytes(output)
