"""Module containing the main HTML shortcode parser class.

"""
from .errors import ShortcodeLoadError, BlockNotFoundError
from decorator import decorator
import re
import logging
import inspect


class HTMLShortcodeParser(object):
    """HTML shortcode parser

    Note that the html snippet used to initialise an object of this class
    is unchanged by all methods.  Instantiate a new object to 
    parse a new snippet.

    Example:
        HTMLShortcodeParser.register_block_processor(MyShortcodeProcessor)
        HTMLShortcodeParser.register_block_processor(MyOtherShortcodeProcessor)
        HTMLShortcodeParser.register_block_processor(MyInlineShortcodeProcessor)

        parser = HTMLShortcodeParser(html_snippet_with_shortcodes)
        html_snippet, extra_snippets = parser.replace_shortcodes_with_html()

    Attributes:
        pattern (regex): Describes a generic HTML element,
            with "tag", "tag_atts" and "content" groups.
            Default regex matches to each top-level HTML element.

    Deleted Attributes:
        get_admin_html (TYPE): Description

    """
    pattern = re.compile(
        r'<(?P<tag>\w*)\s*(?P<tag_atts>[^<>]*)>' +
        r'(?P<content>.*?(?:<(?P=tag).*?>.*?<\/(?P=tag)>)' +
        r'*[^<>]*)<\/(?P=tag)>',
        re.DOTALL)
    _block_processors = {}
    _inline_processors = {}

    def __init__(self, html):
        """Initialise with source HTML snippet

        Args:
            html (str): HTML source

        """
        self._html_snippet = html
        self._extra_html = {}
        for ShortcodeProcessor in self._block_processors.values():
            try:
                ShortcodeProcessor.renderer.reset_counter()
            except AttributeError:
                pass
        for ShortcodeProcessor in self._inline_processors.values():
            try:
                ShortcodeProcessor.renderer.reset_counter()
            except AttributeError:
                pass

    @classmethod
    def register_block_shortcode(cls, ShortcodeProcessor):
        """Register a block shortcode.  

        Args:
            ShortcodeProcessor (class): ShortcodeProcessor-type 
                class to register
        """
        cls._block_processors[ShortcodeProcessor.name] = ShortcodeProcessor

    @classmethod
    def register_inline_shortcode(cls, ShortcodeProcessor):
        """Register an inline shortcode.

        Args:
            ShortcodeProcessor (class): ShortcodeProcessor-type class to register
        """
        cls._inline_processors[
            ShortcodeProcessor.name] = ShortcodeProcessor

    def remove_shortcodes(self, shortcode_names):
        """
        Returns:
            str: source HTML without named shortcodes
        """
        html = self._html_snippet
        for name in shortcode_names:
            try:
                ShortcodeProcessor = self._block_processors[name]
            except KeyError:
                pass

            try:
                ShortcodeProcessor = self._block_processors[name]
            except KeyError:
                print "Shortcode not found %s" % name
                continue

            renderers_matches = ShortcodeProcessor(
                html).renderers_and_matches()

            prev_i = 0
            new_html = ""
            for renderer, match in renderers_matches:
                new_html += html[prev_i:match.start()]
                prev_i = match.end()
            new_html += html[:renderers_matches[-1][1]]
            html = new_html
        return html

    def get_rendered_html(self):
        """replace all shortcodes in the source with their HTML rendering 

        Returns:
            str: fully rendered HTML snippet
        """
        try:
            return self._rendered_html
        except AttributeError:
            pass
        self._rendered_html = ""
        self._extra_html_snippets = self._get_extra_snippets_from_inlines()

        for k in sorted(self._blocks):
            match = self._blocks[k]
            try:
                renderers = self._get_renderers_for_shortcode(
                    match.group(0))
            except ShortcodeLoadError:
                logging.debug("No shortcode")
                self._rendered_html += match.group(0)
                continue
            for renderer in renderers:
                self._rendered_html += renderer.get_html()
                try:
                    renderer.update_extra_html_snippets(
                        self._extra_html_snippets)
                except AttributeError:
                    pass
        return self._rendered_html

    def get_extra_html_snippets(self):
        """Get any extra HTML snippets generated by registered shortcode processors
            when parsing the source HTML

        Returns:
            :obj:`dict` of :obj:`str`: named HTML snippets
        """
        try:
            return self._extra_html_snippets
        except AttributeError:
            self.get_rendered_html()
            return self._extra_html_snippets

    def insert_shortcode(self, block_id, renderer, after=True):
        """ insert a shortcode into the source HTML

        Args:
            block_id (str): ID of HTML block
            renderer (object): ShortcodeRenderer-type instance representing
                the shortcode to add
            after (bool, optional): On True, the shortcode is added immediately
                after the existing block; on False, immediately before.

        Returns:
            str: source HTML with shortcode block inserted

        Raises:
            BlockNotFoundError: when block ID does not match an HTML block
        """
        try:
            match = self._blocks[int(block_id)]
        except KeyError:
            raise BlockNotFoundError
        index = match.end() if after else match.start()
        return self._html_snippet[:index] + "<p>" + renderer.get_shortcode() +"</p>" \
            + self._html_snippet[index:]

    def remove_shortcode(self, block_id):
        """remove a shortcode from the source HTML

        Args:
            block_id (str): ID of HTML block

        Returns:
            str: source HTML with shortcode block removed

        Raises:
            BlockNotFoundError: when block ID does not match an HTML block
        """
        try:
            match = self._matches[block_id]
        except KeyError:
            raise BlockNotFoundError(
                "Shortcode matching ID %s not found" % block_id)
        # next line raises ShortcodeLoadError if not a shortcode
        self._get_renderers_for_shortcode(match.group(0))
        return self._html_snippet[:match.start()] + \
            self._html_snippet[match.end():]

    def get_renderers(self, block_id):
        """get renderers for a shortcode block

        Args:
            block_id (str): ID of HTML block

        Returns:
            :obj:`list` of :obj:`BaseShortcodeRenderer`: Description

        Raises:
            BlockNotFoundError: when block ID does not match an HTML block
            ShortcodeLoadError: when block is found but is not a shortcode
        """
        try:
            match = self._matches[block_id]
        except KeyError:
            raise BlockNotFoundError
        # next line raises ShortcodeLoadError if not a shortcode
        return self._get_renderers_for_shortcode(match.group(0))

    @staticmethod
    def _remove_block_markup(html):
        html = html.replace("<div class=\"shortcode\"><!-- shortcode -->", "")
        html = html.replace("<!-- end_shortcode --></div>", "")
        return html

    def _add_attrs_to_block(*attr_names):
        """decorator factory that adds html attributes to a block

        Args:
            *attr_names (:obj:`list` of :obj:`str`): HTML attribute names.
                these correspond to the return values of the function
                being decorated

        Returns:
            func: The decorator
        """

        def decorator_body(attr_method, *args, **kwargs):
            """Body of decorator that adds HTML attributes to a block

            Args:
                attr_method (func): function to decorate
                *args: Description
                **kwargs: Description

            Other Arguments:
                self(:obj:`HTMLShortcodeParser`): object which method to be 
                    decorated belongs to
                match(:obj:`re.MatchObject`): HTML block to modify

            Returns:
                str: HTML of modified block

            """
            attr_vals = attr_method(*args, **kwargs)
            match = args[1]

            def make_attr_str(acc, pair):
                """Callback for reduce to make HTML attribute string

                Args:
                    acc (str): Accumulated attribute string
                    pair (tuple): (name, val) attribute pair

                Returns:
                    str: Accumulated attribute string
                """
                if pair[1] is None:
                    return acc
                out = (acc,) + pair
                return "%s %s=\"%s\"" % out

            replacement_attrs = reduce(
                make_attr_str,
                zip(attr_names, attr_vals),
                match.group("tag_attrs"))

            return "<%s %s>%s</%s>" % (
                match.group('tag'),
                replacement_attrs,
                match.group('content'),
                match.group('tag'))
        return decorator(decorator_body)

    def mark_up_block_shortcodes(self):
        start = self._remove_block_markup(self._html_snippet)
        out = start

        for name, ShortcodeProcessor in self._block_processors.iteritems():
            proc = ShortcodeProcessor(out)

            def _wrap(match):
                renderer = proc._shortcode_to_renderer(match)
                return renderer.get_wrapped_shortcode()
            # print repr(ShortcodeProcessor)
            out = re.sub(proc.pattern, _wrap, out)

        # clean up
        out = re.sub(r"<p[^>]*><div", "<div", out)
        out = out.replace("</div></p>", "</div>")
        return out

    def _get_html_with_inlines_replaced(self):
        """get source snippet with all inline shortcodes substituted
            with HTML

        Returns:
            str: rendered HTML
        """
        try:
            return self._inlines_replaced
        except AttributeError:
            pass
        self._inlines_replaced = self._html_snippet
        self._inline_html_snippets = {}
        for name, ShortcodeProcessor in self._inline_processors.iteritems():
            logging.debug("Inline")
            logging.debug(ShortcodeProcessor)
            self._inlines_replaced = ShortcodeProcessor(
                self._inlines_replaced).get_rendered_html(
                extra_html_snippets=self._inline_html_snippets)
            logging.debug(self._inlines_replaced)
        return self._inlines_replaced

    def _get_extra_snippets_from_inlines(self):
        """get any extra snippets defined by inline shortcode processors

        Returns:
            :obj: `dict` of :obj:`str`: Named html snippets

        """
        try:
            return self._inline_html_snippets
        except AttributeError:
            self._get_html_with_inlines_replaced()
            return self._inline_html_snippets

    @property
    def _blocks(self):
        """
        Returns:
            :obj: `dict` of :obj:`re.MatchObject`: HTML blocks indexed by ID
        """
        try:
            return self._c_matches
        except AttributeError:
            pass
        self._c_matches = dict([
            (i + 1, match)
            for i, match in enumerate(self.pattern.finditer(
                self._get_html_with_inlines_replaced()))])

        return self._c_matches

    def _get_renderers_for_shortcode(self, shortcode_html):
        """Get all markup renderers for a shortcode instance

        Args:
            shortcode_html (str): html of shortcode block

        Returns:
            :obj: `list` of :obj:`BaseShortcodeRenderer`

        Raises:
            ShortcodeLoadError: if shortcode_html is not a recognised shortcode
        """
        for name, ShortcodeProcessor in self._block_processors.iteritems():
            renderers = ShortcodeProcessor(shortcode_html).renderers()
            if renderers:
                return renderers
        raise ShortcodeLoadError

    def _get_name_of_shortcode(self, shortcode_html):
        """Get the machine name of a shortcode instance

        Args:
            shortcode_html (str): html of shortcode block

        Returns:
            str: name of matching shortcode

        Raises:
            ShortcodeLoadError: Description
        """
        for name, ShortcodeProcessor in self._block_processors.iteritems():
            try:
                return name
            except ShortcodeLoadError:
                continue
        raise ShortcodeLoadError

    def _get_id_of_match(self, match):
        """Returns the unique ID of a matched block

        Args:
            match (TYPE): Description

        Returns:
            TYPE: Description
        """
        return match.start()

    def _get_shortcode_type(self, match):
        """Returns the name of the processor for a matched shortcode block,
        or raises ShortcodeLoadError if the block
        is not a recognised shortcode 

        Args:
            match (TYPE): Description

        Returns:
            TYPE: Description
        """
        try:
            shortcode_name, shortcode = self._get_renderers_for_shortcode(
                match.group(0))
            return shortcode_name
        except ShortcodeLoadError:
            return None
