# -*- coding: utf-8 -*-
# See LICENSE.txt for licensing terms

'''
This module provides one useful class:  NodeHandler

The NodeHandler class is designed to be subclassed.  Each subclass
should support the processing that createpdf.RstToPdf needs to do
on a particular type of node that could appear in a document tree.

When the subclass is defined, it should reference NodeHandler as
the first base class, and one or more docutils node classes as
subsequent base classes.

These docutils node classes will not actually wind up in the
base classes of the subclass.  Instead, they will be used as
keys in a dispatch dictionary which is used to find the correct
NodeHandler subclass to use to process an instance of a given
docutils node class.

When an instance of createpdf.RstToPdf is created, a NodeHandler
instance will be called to return dispatchers for gather_elements
and gather_pdftext, wrapped up as methods of the createpdf.RstToPdf
class.

When a dispatcher is called, it will dispatch to the correct subclass
to handle the given docutils node instance.

If no NodeHandler subclass has been created to handle that particular
type of docutils node, then default processing will occur and a warning
will be logged.
'''

from copy import copy
import inspect
import types

import docutils.nodes
from docutils.utils import smartquotes

from .flowables import BoundByWidth, TocEntry
from .log import log, nodeid


class MetaHelper(type):
    """MetaHelper is designed to generically enable a few of the benefits of
    using metaclasses by encapsulating some of the complexity of setting
    them up.

    If a base class uses MetaHelper (by assigning __metaclass__ = MetaHelper),
    then that class (and its metaclass inheriting subclasses) can control
    class creation behavior by defining a couple of helper functions.

    1) A base class can define a _classpreinit function.  This function
       is called during __new__ processing of the class object itself,
       but only during subclass creation (not when the class defining
       the _classpreinit is itself created).

       The subclass object does not yet exist at the time _classpreinit
       is called.  _classpreinit accepts all the parameters of the
       __new__ function for the class itself (not the same as the __new__
       function for the instantiation of class objects!) and must return
       a tuple of the same objects.  A typical use of this would be to
       modify the class bases before class creation.

    2) Either a base class or a subclass can define a _classinit() function.
       This function will be called immediately after the actual class has
       been created, and can do whatever setup is required for the class.
       Note that every base class (but not every subclass) which uses
       MetaHelper MUST define _classinit, even if that definition is None.

     MetaHelper also places an attribute into each class created with it.
     _baseclass is set to None if this class has no superclasses which
     also use MetaHelper, or to the first such MetaHelper-using baseclass.
     _baseclass can be explicitly set inside the class definition, in
     which case MetaHelper will not override it.
    """

    def __new__(clstype, name, bases, clsdict):
        # Our base class is the first base in the class definition which
        # uses MetaHelper, or None if no such base exists.
        base = ([x for x in bases if type(x) is MetaHelper] + [None])[0]

        # Only set our base into the class if it has not been explicitly
        # set
        clsdict.setdefault('_baseclass', base)

        # See if the base class definied a preinit function, and call it
        # if so.
        preinit = getattr(base, '_classpreinit', None)
        if preinit is not None:
            clstype, name, bases, clsdict = preinit(clstype, name, bases, clsdict)

        # Delegate the real work to type
        return type.__new__(clstype, name, bases, clsdict)

    def __init__(cls, name, bases, clsdict):
        # Let type build the class for us
        type.__init__(cls, name, bases, clsdict)
        # Call the class's initialization function if defined
        if cls._classinit is not None:
            cls._classinit()


class NodeHandler(metaclass=MetaHelper):
    """NodeHandler classes are used to dispatch
    to the correct class to handle some node class
    type, via a dispatchdict in the main class.
    """

    dispatchdict = {}

    @classmethod
    def _classpreinit(baseclass, clstype, name, bases, clsdict):
        # _classpreinit is called before the actual class is built
        # Perform triage on the class bases to separate actual
        # inheritable bases from the target docutils node classes
        # which we want to dispatch for.

        new_bases = []
        targets = []
        for target in bases:
            if target is not object:
                (targets, new_bases)[issubclass(target, NodeHandler)].append(target)
        clsdict['_targets'] = targets
        return clstype, name, tuple(new_bases), clsdict

    @classmethod
    def _classinit(cls):
        # _classinit() is called once the subclass has actually
        # been created.

        # For the base class, just add a dispatch dictionary
        if cls._baseclass is None:
            cls.dispatchdict = {}
            return

        # for subclasses, instantiate them, and then add
        # the class to the dispatch dictionary for each of its targets.
        self = cls()
        for target in cls._targets:
            if cls.dispatchdict.setdefault(target, self) is not self:
                t = repr(target)
                old = repr(cls.dispatchdict[target])
                new = repr(self)
                log.debug(
                    'Dispatch handler %s for node type %s overridden by %s'
                    % (old, t, new)
                )
                cls.dispatchdict[target] = self

    @staticmethod
    def getclassname(obj):
        cln = repr(obj.__class__)
        info = cln.split("'")
        if len(info) == 3:
            return info[1]
        return cln

    def log_unknown(self, node, during):
        if not hasattr(self, 'unkn_node'):
            self.unkn_node = set()
        cln = self.getclassname(node)
        if cln not in self.unkn_node:
            self.unkn_node.add(cln)
            log.warning("Unkn. node (self.%s): %s [%s]", during, cln, nodeid(node))
            try:
                log.debug(node)
            except (UnicodeDecodeError, UnicodeEncodeError):
                log.debug(repr(node))

    def findsubclass(self, node, during):
        handlerinfo = '%s.%s' % (self.getclassname(self), during)
        log.debug("%s: %s", handlerinfo, self.getclassname(node))
        log.debug("%s: [%s]", handlerinfo, nodeid(node))
        try:
            log.debug("%s: %s", handlerinfo, node)
        except (UnicodeDecodeError, UnicodeEncodeError):
            log.debug("%s: %r", handlerinfo, node)
        log.debug("")

        # Dispatch to the first matching class in the MRO

        dispatchdict = self.dispatchdict
        for baseclass in inspect.getmro(node.__class__):
            result = dispatchdict.get(baseclass)
            if result is not None:
                break
        else:
            self.log_unknown(node, during)
            result = self
        return result

    def __call__(self, client):
        '''Get the dispatchers, wrapped up as methods for the client'''
        textdispatch = types.MethodType(self.textdispatch, client)
        elemdispatch = types.MethodType(self.elemdispatch, client)
        return textdispatch, elemdispatch

    # This overridable attribute will be set true in the instance
    # if handling a sphinx document

    sphinxmode = False

    # Begin overridable attributes and methods for elemdispatch

    def gather_elements(self, client, node, style):
        return client.gather_elements(node, style=style)

    def _get_non_default_values_from_style(self, style):
        """Return a dictionary of all the items in the style that are changed from their default value"""
        items = style.__dict__.copy()
        defaults = style.defaults
        for key in defaults.keys():
            if defaults[key] == items[key]:
                del items[key]
        return items

    def getstyle(self, client, node, style):
        try:
            if node['classes'] and node['classes'][0]:
                for n in range(len(node['classes'])):
                    if node['classes'][n] in client.styles.StyleSheet:
                        if n == 0:
                            style = client.styles[node['classes'][n]]
                        else:
                            # merge the non-default properties of this style into the style we currently have
                            style = copy(style)
                            items = self._get_non_default_values_from_style(
                                client.styles[node['classes'][n]]
                            )
                            items.pop("parent", None)
                            name = f"{style.__dict__['name']}-{items['name']}"
                            items['name'] = name
                            style.__dict__.update(items)
                    else:
                        log.info(
                            "Unknown class %s, ignoring. [%s]",
                            node['classes'][n],
                            nodeid(node),
                        )
        except TypeError:  # Happens when a docutils.node.Text reaches here
            pass

        if style is None or style == client.styles['bodytext']:
            style = client.styles.styleForNode(node)
        return style

    def getelements(self, client, node, style):
        style = self.getstyle(client, node, style)
        elements = self.gather_elements(client, node, style)

        # Make all the sidebar cruft unreachable
        # if style.__dict__.get('float','None').lower() !='none':
        # node.elements=[Sidebar(node.elements,style)]
        # elif 'width' in style.__dict__:

        if 'width' in style.__dict__:
            elements = [BoundByWidth(style.width, elements, style, mode="shrink")]

        return elements

    # End overridable attributes and methods for elemdispatch

    def elemdispatch(self, client, node, style=None):
        self = self.findsubclass(node, 'elemdispatch')

        # set anchors for internal references
        try:
            for i in node['ids']:
                client.pending_targets.append(i)
        except TypeError:  # Happens with docutils.node.Text
            pass

        elements = self.getelements(client, node, style)

        if node.line and client.debugLinesPdf:
            elements.insert(0, TocEntry(client.depth - 1, 'LINE-%s' % node.line))
        node.elements = elements
        return elements

    # Begin overridable attributes and methods for textdispatch

    pre = ''
    post = ''

    def get_pre_post(self, client, node, replaceEnt):
        return self.pre, self.post

    def get_text(self, client, node, replaceEnt):
        return client.gather_pdftext(node)

    def apply_replacements(self, text, smarty, node):
        # Try to be clever about when to replace characters
        if node.__class__ in (
            docutils.nodes.paragraph,
            docutils.nodes.block_quote,
            docutils.nodes.title,
        ):
            return smartquotes.smartyPants(text, smarty)
        return text

    # End overridable attributes and methods for textdispatch

    def textdispatch(self, client, node, replaceEnt=True):
        self = self.findsubclass(node, 'textdispatch')
        pre, post = self.get_pre_post(client, node, replaceEnt)
        text = self.get_text(client, node, replaceEnt)
        text = pre + text + post

        try:
            log.debug("%s.textdispatch: %s" % (self.getclassname(self), text))
        except UnicodeDecodeError:
            pass

        text = self.apply_replacements(text, client.smartypants_attributes, node)
        node.pdftext = text
        return text
