# Authors: David Goodger, Ueli Schlaepfer
# Contact: goodger@users.sourceforge.net
# Revision: $Revision: 3186 $
# Date: $Date: 2005-04-07 21:51:45 +0200 (Thu, 07 Apr 2005) $
# Copyright: This module has been placed in the public domain.

"""
Transforms needed by most or all documents:

- `Decorations`: Generate a document's header & footer.
- `Messages`: Placement of system messages stored in
  `nodes.document.transform_messages`.
- `TestMessages`: Like `Messages`, used on test runs.
- `FinalReferences`: Resolve remaining references.
"""

__docformat__ = 'reStructuredText'

import re
import sys
import time
from docutils import nodes, utils
from docutils.transforms import TransformError, Transform


class Decorations(Transform):

    """
    Populate a document's decoration element (header, footer).
    """

    default_priority = 820

    def apply(self):
        header_nodes = self.generate_header()
        if header_nodes:
            decoration = self.document.get_decoration()
            header = decoration.get_header()
            header.extend(header_nodes)
        footer_nodes = self.generate_footer()
        if footer_nodes:
            decoration = self.document.get_decoration()
            footer = decoration.get_footer()
            footer.extend(footer_nodes)

    def generate_header(self):
        return None

    def generate_footer(self):
        # @@@ Text is hard-coded for now.
        # Should be made dynamic (language-dependent).
        settings = self.document.settings
        if settings.generator or settings.datestamp or settings.source_link \
               or settings.source_url:
            text = []
            if settings.source_link and settings._source \
                   or settings.source_url:
                if settings.source_url:
                    source = settings.source_url
                else:
                    source = utils.relative_path(settings._destination,
                                                 settings._source)
                text.extend([
                    nodes.reference('', 'View document source',
                                    refuri=source),
                    nodes.Text('.\n')])
            if settings.datestamp:
                datestamp = time.strftime(settings.datestamp, time.gmtime())
                text.append(nodes.Text('Generated on: ' + datestamp + '.\n'))
            if settings.generator:
                text.extend([
                    nodes.Text('Generated by '),
                    nodes.reference('', 'Docutils', refuri=
                                    'http://docutils.sourceforge.net/'),
                    nodes.Text(' from '),
                    nodes.reference('', 'reStructuredText', refuri='http://'
                                    'docutils.sourceforge.net/rst.html'),
                    nodes.Text(' source.\n')])
            return [nodes.paragraph('', '', *text)]
        else:
            return None


class Messages(Transform):

    """
    Place any system messages generated after parsing into a dedicated section
    of the document.
    """

    default_priority = 860

    def apply(self):
        unfiltered = self.document.transform_messages
        threshold = self.document.reporter.report_level
        messages = []
        for msg in unfiltered:
            if msg['level'] >= threshold and not msg.parent:
                messages.append(msg)
        if messages:
            section = nodes.section(classes=['system-messages'])
            # @@@ get this from the language module?
            section += nodes.title('', 'Docutils System Messages')
            section += messages
            self.document.transform_messages[:] = []
            self.document += section


class FilterMessages(Transform):

    """
    Remove system messages below verbosity threshold.
    """

    default_priority = 870

    def apply(self):
        visitor = SystemMessageFilterVisitor(self.document)
        self.document.walk(visitor)


class SystemMessageFilterVisitor(nodes.SparseNodeVisitor):

    def unknown_visit(self, node):
        pass

    def visit_system_message(self, node):
        if node['level'] < self.document.reporter.report_level:
            node.parent.remove(node)


class TestMessages(Transform):

    """
    Append all post-parse system messages to the end of the document.
    """

    default_priority = 890

    def apply(self):
        for msg in self.document.transform_messages:
            if not msg.parent:
                self.document += msg


class FinalChecks(Transform):

    """
    Perform last-minute checks and transforms.

    - Check for dangling references (incl. footnote & citation).
    - Check for illegal transitions, move transitions.
    """

    default_priority = 840

    def apply(self):
        visitor = FinalCheckVisitor(
            self.document,
            self.document.transformer.unknown_reference_resolvers)
        self.document.walk(visitor)
        if self.document.settings.expose_internals:
            visitor = InternalAttributeExposer(self.document)
            self.document.walk(visitor)
        # *After* resolving all references, check for unreferenced
        # targets:
        for target in self.document.traverse():
            if isinstance(target, nodes.target) and not target.referenced:
                if target['names']:
                    naming = target['names'][0]
                elif target['ids']:
                    naming = target['ids'][0]
                else:
                    # Hack: Propagated targets always have their refid
                    # attribute set.
                    naming = target['refid']
                self.document.reporter.info(
                    'Hyperlink target "%s" is not referenced.'
                    % naming, base_node=target)


class FinalCheckVisitor(nodes.SparseNodeVisitor):
    
    def __init__(self, document, unknown_reference_resolvers):
        nodes.SparseNodeVisitor.__init__(self, document)
        self.document = document
        self.unknown_reference_resolvers = unknown_reference_resolvers

    def unknown_visit(self, node):
        pass

    def visit_reference(self, node):
        if node.resolved or not node.hasattr('refname'):
            return
        refname = node['refname']
        id = self.document.nameids.get(refname)
        if id is None:
            for resolver_function in self.unknown_reference_resolvers:
                if resolver_function(node):
                    break
            else:
                if self.document.nameids.has_key(refname):
                    msg = self.document.reporter.error(
                        'Duplicate target name, cannot be used as a unique '
                        'reference: "%s".' % (node['refname']), base_node=node)
                else:
                    msg = self.document.reporter.error(
                        'Unknown target name: "%s".' % (node['refname']),
                        base_node=node)
                msgid = self.document.set_id(msg)
                prb = nodes.problematic(
                      node.rawsource, node.rawsource, refid=msgid)
                prbid = self.document.set_id(prb)
                msg.add_backref(prbid)
                node.parent.replace(node, prb)
        else:
            del node['refname']
            node['refid'] = id
            self.document.ids[id].note_referenced_by(id=id)
            node.resolved = 1

    visit_footnote_reference = visit_citation_reference = visit_reference

    def visit_transition(self, node):
        """
        Move transitions at the end of sections up the tree.  Complain
        on transitions after a title, at the beginning or end of the
        document, and after another transition.

        For example, transform this::

            <section>
                ...
                <transition>
            <section>
                ...

        into this::

            <section>
                ...
            <transition>
            <section>
                ...
        """
        index = node.parent.index(node)
        error = None
        if (index == 0 or
            isinstance(node.parent[0], nodes.title) and
            (index == 1 or
             isinstance(node.parent[1], nodes.subtitle) and
             index == 2)):
            assert (isinstance(node.parent, nodes.document) or
                    isinstance(node.parent, nodes.section))
            error = self.document.reporter.error(
                'Document or section may not begin with a transition.',
                line=node.line)
        elif isinstance(node.parent[index - 1], nodes.transition):
            error = self.document.reporter.error(
                'At least one body element must separate transitions; '
                'adjacent transitions are not allowed.', line=node.line)
        if error:
            # Insert before node and update index.
            node.parent.insert(index, error)
            index += 1
        assert index < len(node.parent)
        if index != len(node.parent) - 1:
            # No need to move the node.
            return
        # Node behind which the transition is to be moved.
        sibling = node
        # While sibling is the last node of its parent.
        while index == len(sibling.parent) - 1:
            sibling = sibling.parent
            # If sibling is the whole document (i.e. it has no parent).
            if sibling.parent is None:
                # Transition at the end of document.  Do not move the
                # transition up, and place an error behind.
                error = self.document.reporter.error(
                    'Document may not end with a transition.',
                    line=node.line)
                node.parent.insert(node.parent.index(node) + 1, error)
                return
            index = sibling.parent.index(sibling)
        # Remove the original transition node.
        node.parent.remove(node)
        # Insert the transition after the sibling.
        sibling.parent.insert(index + 1, node)


class InternalAttributeExposer(nodes.GenericNodeVisitor):

    def __init__(self, document):
        nodes.GenericNodeVisitor.__init__(self, document)
        self.internal_attributes = document.settings.expose_internals

    def default_visit(self, node):
        for att in self.internal_attributes:
            value = getattr(node, att, None)
            if value is not None:
                node['internal:' + att] = value
