from .base import DocumentParser
from django.utils.html import escape
from .tables import Table, Cell


class GoogleDocumentParser(DocumentParser):
    """Default DocumentParser for Google Docs, taking a JSON Google Doc and converting upon parse() into a list of
    {'type': type, 'value': value} intermediate elements"""

    def __init__(self, document):
        self.document = document

    def elements_to_html(self, paragraph, outer_tag="p"):
        """
        Compile a paragraph into a HTML string, optionally with semantic markup for styles.
        Returns a dictionary of the form:
        {
            'html': 'HTML content',
            'embeds': '[A list of embedded object IDs that were found in the paragraph]'
        }
        """
        output = []
        embeds = []

        for element in paragraph["elements"]:
            if "inlineObjectElement" in element:
                embeds.append(element["inlineObjectElement"]["inlineObjectId"])
                continue

            text_run = element.get("textRun")
            if not text_run:
                continue

            content = text_run["content"].strip("\n")
            if not content:
                continue

            prefixes = []
            suffixes = []

            style = text_run["textStyle"]
            if style.get("bold"):
                prefixes.append("<b>")
                suffixes.append("</b>")
            if style.get("italic"):
                prefixes.append("<i>")
                suffixes.append("</i>")
            if style.get("underline"):
                prefixes.append("<u>")
                suffixes.append("</u>")
            if style.get("baselineOffset") == "SUPERSCRIPT":
                prefixes.append("<sup>")
                suffixes.append("</sup>")
            elif style.get("baselineOffset") == "SUBSCRIPT":
                prefixes.append("<sub>")
                suffixes.append("</sub>")
            if style.get("strikethrough"):
                prefixes.append("<s>")
                suffixes.append("</s>")
            if style.get("link"):
                url = style["link"].get("url")
                # Links without a 'url' field are local bookmark/heading references;
                # skip these as there's no direct equivalent in Wagtail content
                # https://developers.google.com/docs/api/reference/rest/v1/documents#Link
                if url:
                    prefixes.append('<a href="{}">'.format(escape(url)))
                    suffixes.append("</a>")

            suffixes.reverse()
            html = "".join(prefixes) + escape(content) + "".join(suffixes)
            output.append(html)

        if output:
            inner_html = "".join(output)
            html = (
                f"<{outer_tag}>" + inner_html + f"</{outer_tag}>"
                if outer_tag
                else inner_html
            )
        else:
            html = ""
        return {
            "html": html,
            "embeds": embeds,
        }

    def process_embedded_object(self, embed_id):
        embed = self.document["inlineObjects"][embed_id]
        # Currently we only handle images
        image_props = embed["inlineObjectProperties"]["embeddedObject"].get(
            "imageProperties"
        )
        title = embed["inlineObjectProperties"]["embeddedObject"].get("title", "")
        if image_props:
            return {"type": "image", "value": image_props["contentUri"], "title": title}

    def process_list(self, list_items):
        """
        Return HTML for a series of list items.
        Ordered/unordered lists are only respected at the top level.
        """
        list_id = list_items[0]["list_id"]
        list_definition = self.document["lists"][list_id]
        # ordered lists define a glyphType (DECIMAL, ALPHA etc) whereas unordered lists define
        # a constant glyphSymbol
        is_ordered = (
            "glyphType" in list_definition["listProperties"]["nestingLevels"][0]
        )
        list_tag = "ol" if is_ordered else "ul"

        # Construct an annotated list that can be efficiently converted into a nested tree
        parts = []
        previous_level = None
        info = {}
        for item in list_items:
            # Update previous info item
            if previous_level is None or previous_level < item["level"]:
                info["has_children"] = True

            if previous_level is not None and previous_level > item["level"]:
                info["num_to_close"] = previous_level - item["level"]

            info = {"html": item["html"]}
            previous_level = item["level"]
            parts.append(info)

        if previous_level is not None:
            # close last leaf
            info["num_to_close"] = previous_level

        # Generated nested tree
        html = f"<{list_tag}>"
        for part in parts:
            html += "<li>" + part["html"]
            html += f"<{list_tag}>" if part.get("has_children") else "</li>"
            for i in range(0, part.get("num_to_close", 0)):
                html += f"</{list_tag}></li>"
        html += f"</{list_tag}>"
        return html

    def process_table(self, table):
        imported_rows = [
            [Cell(self.get_cell_text(cell)) for cell in row["tableCells"]]
            for row in table["tableRows"]
        ]
        return Table(imported_rows)

    @staticmethod
    def get_cell_text(cell):
        """
        Get table cell text
        """
        text = ""
        for content_element in cell["content"]:
            if "paragraph" not in content_element:
                continue
            for paragraph_element in content_element["paragraph"]["elements"]:
                if "textRun" not in paragraph_element:
                    continue
                text += paragraph_element["textRun"]["content"]
        return text.strip()

    def parse(self):
        """
        Parse the document and return a set of intermediate {'type': type, 'value': value} blocks that represent it.
        """
        current_block = []
        unprocessed_embeds = []
        blocks = []

        def close_current_block():
            if current_block:
                blocks.append({"type": "html", "value": "".join(current_block)})
                current_block.clear()
            if unprocessed_embeds:
                for embed_id in unprocessed_embeds:
                    embed = self.process_embedded_object(embed_id)
                    if embed:
                        blocks.append(embed)
                unprocessed_embeds.clear()

        body = self.document["body"]

        # List parsing is rather messy because list items are returned as a
        # series of paragaph objects rather than a nested structure.
        current_list = []

        def close_current_list():
            if current_list:
                html = self.process_list(current_list)
                current_block.append(html)
                current_list.clear()

        for part in body["content"]:
            # Part can contain one of sectionBreak, table, tableOfContents, paragraph
            # Currently we only process paragraph and table
            if "table" in part:
                close_current_block()
                close_current_list()
                table = self.process_table(part["table"])
                blocks.append({"type": "table", "value": table})

            if "paragraph" not in part:
                continue

            paragraph = part["paragraph"]
            style = paragraph["paragraphStyle"].get("namedStyleType")
            if style == "HEADING_1":
                close_current_list()
                close_current_block()
                try:
                    blocks.append(
                        {
                            "type": "heading",
                            "value": paragraph["elements"][0]["textRun"][
                                "content"
                            ].strip(),
                        }
                    )
                except KeyError:
                    pass
            elif "bullet" in paragraph:  # We're in a list
                content = self.elements_to_html(paragraph, outer_tag=None)
                current_list.append(
                    {
                        "html": content["html"],
                        "level": paragraph["bullet"].get("nestingLevel", 0),
                        "list_id": paragraph["bullet"]["listId"],
                    }
                )
                unprocessed_embeds += content["embeds"]
            else:
                close_current_list()

                if paragraph["paragraphStyle"]["namedStyleType"] == "HEADING_2":
                    outer_tag = "h2"
                elif paragraph["paragraphStyle"]["namedStyleType"] == "HEADING_3":
                    outer_tag = "h3"
                elif paragraph["paragraphStyle"]["namedStyleType"] == "HEADING_4":
                    outer_tag = "h4"
                elif paragraph["paragraphStyle"]["namedStyleType"] == "HEADING_5":
                    outer_tag = "h5"
                elif paragraph["paragraphStyle"]["namedStyleType"] == "HEADING_6":
                    outer_tag = "h6"
                else:
                    outer_tag = "p"

                content = self.elements_to_html(paragraph, outer_tag=outer_tag)
                unprocessed_embeds += content["embeds"]
                if content["html"]:
                    current_block.append(content["html"])

                # If any embeds were encountered since the last paragraph processed
                # (including within headings / list items that aren't processed as
                # standard paragraphs), close this block and start a new one so that
                # the embed block is in the correct place in the text
                if unprocessed_embeds:
                    close_current_block()

        close_current_list()
        close_current_block()
        return {"title": self.document["title"], "elements": blocks}
