import csv
from itertools import zip_longest
import os
import re
import string

from docutils import nodes, utils
from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.tables import Table


def libpr_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    ref = "https://github.com/esphome/esphome-core/pull/{}".format(text)
    return [make_link_node(rawtext, "core#{}".format(text), ref, options)], []


def yamlpr_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    ref = "https://github.com/esphome/esphome/pull/{}".format(text)
    return [make_link_node(rawtext, "esphome#{}".format(text), ref, options)], []


def docspr_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    ref = "https://github.com/esphome/esphome-docs/pull/{}".format(text)
    return [make_link_node(rawtext, "docs#{}".format(text), ref, options)], []


def ghuser_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    ref = "https://github.com/{}".format(text)
    return [make_link_node(rawtext, "@{}".format(text), ref, options)], []


value_re = re.compile(r"^(.*)\s*<(.*)>$")
DOXYGEN_LOOKUP = {}
for s in string.ascii_lowercase + string.digits:
    DOXYGEN_LOOKUP[s] = s
for s in string.ascii_uppercase:
    DOXYGEN_LOOKUP[s] = "_{}".format(s.lower())
DOXYGEN_LOOKUP[":"] = "_1"
DOXYGEN_LOOKUP["_"] = "__"
DOXYGEN_LOOKUP["."] = "_8"


def split_text_value(value):
    match = value_re.match(value)
    if match is None:
        return None, value
    return match.group(1), match.group(2)


def encode_doxygen(value):
    value = value.split("/")[-1]
    try:
        return "".join(DOXYGEN_LOOKUP[s] for s in value)
    except KeyError:
        raise ValueError("Unknown character in doxygen string! '{}'".format(value))


def apiref_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    text, value = split_text_value(text)
    if text is None:
        text = "API Reference"
    ref = "/api/{}.html".format(encode_doxygen(value))
    return [make_link_node(rawtext, text, ref, options)], []


def apiclass_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    text, value = split_text_value(text)
    if text is None:
        text = value
    ref = "/api/classesphome_1_1{}.html".format(encode_doxygen(value))
    return [make_link_node(rawtext, text, ref, options)], []


def apistruct_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    text, value = split_text_value(text)
    if text is None:
        text = value
    ref = "/api/structesphome_1_1{}.html".format(encode_doxygen(value))
    return [make_link_node(rawtext, text, ref, options)], []


def ghedit_role(name, rawtext, text, lineno, inliner, options=None, content=None):
    path = os.path.relpath(
        inliner.document.current_source, inliner.document.settings.env.app.srcdir
    )
    ref = "https://github.com/esphome/esphome-docs/blob/current/{}".format(path)
    return [make_link_node(rawtext, "Edit this page on GitHub", ref, options)], []


def make_link_node(rawtext, text, ref, options=None):
    options = options or {}
    node = nodes.reference(rawtext, utils.unescape(text), refuri=ref, **options)
    return node


# https://stackoverflow.com/a/3415150/8924614
def grouper(n, iterable, fillvalue=None):
    """Pythonic way to iterate over sequence, 4 items at a time.

    grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx
    """
    args = [iter(iterable)] * n
    return zip_longest(fillvalue=fillvalue, *args)


# Based on https://www.slideshare.net/doughellmann/better-documentation-through-automation-creating-docutils-sphinx-extensions
class ImageTableDirective(Table):

    option_spec = {
        "columns": directives.positive_int,
    }

    def run(self):
        cols = self.options.get("columns", 3)

        items = []

        data = list(csv.reader(self.content))
        for row in data:
            if not row:
                continue
            name, page, image = row[0:3]
            link = page.strip()
            if link.startswith("http"):
                pass
            else:
                if not link.startswith("/"):
                    link = "/{}".format(link)
                if ".html" not in link:
                    link += ".html"
            category = None
            dark_invert = False
            if len(row) == 4:
                if row[3].strip() == "dark-invert":
                    dark_invert = True
                else:
                    category = row[3].strip()
            if len(row) == 5 and row[4].strip() == "dark-invert":
                dark_invert = True
            items.append(
                {
                    "name": name.strip(),
                    "link": link,
                    "image": "/images/{}".format(image.strip()),
                    "category": category,
                    "dark_invert": dark_invert,
                }
            )

        title, messages = self.make_title()
        table = nodes.table()
        table["classes"].append("table-center")
        table["classes"].append("colwidths-given")

        # Set up column specifications based on widths
        tgroup = nodes.tgroup(cols=cols)
        table += tgroup
        tgroup.extend(nodes.colspec(colwidth=1) for _ in range(cols))

        tbody = nodes.tbody()
        tgroup += tbody
        rows = []
        for value in grouper(cols, items):
            trow = nodes.row()
            for cell in value:
                entry = nodes.entry()
                if cell is None:
                    entry += nodes.paragraph()
                    trow += entry
                    continue
                name = cell["name"]
                link = cell["link"]
                image = cell["image"]
                reference_node = nodes.reference(refuri=link)
                img = nodes.image(uri=directives.uri(image), alt=name)
                img["classes"].append("component-image")
                if cell["dark_invert"]:
                    img["classes"].append("dark-invert")
                reference_node += img
                para = nodes.paragraph()
                para += reference_node
                entry += para
                trow += entry
            rows.append(trow)

            trow = nodes.row()
            for cell in value:
                entry = nodes.entry()
                if cell is None:
                    entry += nodes.paragraph()
                    trow += entry
                    continue
                name = cell["name"]
                link = cell["link"]
                ref = nodes.reference(name, name, refuri=link)
                para = nodes.paragraph()
                para += ref
                entry += para
                cat_text = cell["category"]
                if cat_text:
                    cat = nodes.paragraph(text=cat_text)
                    entry += cat
                trow += entry
            rows.append(trow)
        tbody.extend(rows)

        self.add_name(table)
        if title:
            table.insert(0, title)

        return [table] + messages


class PinTableDirective(Table):
    option_spec = {}

    def run(self):
        items = []

        data = list(csv.reader(self.content))
        for row in data:
            if not row:
                continue
            if len(row) == 3:
                items.append((row[0], row[1], True))
            else:
                items.append((row[0], row[1], False))

        col_widths = self.get_column_widths(2)
        title, messages = self.make_title()
        table = nodes.table()

        # Set up column specifications based on widths
        tgroup = nodes.tgroup(cols=2)
        table += tgroup
        tgroup.extend(nodes.colspec(colwidth=col_width) for col_width in col_widths)

        thead = nodes.thead()
        tgroup += thead
        trow = nodes.row()
        thead += trow
        trow.extend(
            nodes.entry(h, nodes.paragraph(text=h)) for h in ("Pin", "Function")
        )

        tbody = nodes.tbody()
        tgroup += tbody
        for name, func, important in items:
            trow = nodes.row()
            entry = nodes.entry()
            para = nodes.paragraph()
            para += nodes.literal(text=name)
            entry += para
            trow += entry

            entry = nodes.entry()
            if important:
                para = nodes.paragraph()
                para += nodes.strong(text=func)
            else:
                para = nodes.paragraph(text=func)
            entry += para
            trow += entry
            tbody += trow

        self.add_name(table)
        if title:
            table.insert(0, title)

        return [table] + messages


def setup(app):
    app.add_role("libpr", libpr_role)
    app.add_role("corepr", libpr_role)
    app.add_role("yamlpr", yamlpr_role)
    app.add_role("esphomepr", yamlpr_role)
    app.add_role("docspr", docspr_role)
    app.add_role("ghuser", ghuser_role)
    app.add_role("apiref", apiref_role)
    app.add_role("apiclass", apiclass_role)
    app.add_role("apistruct", apistruct_role)
    app.add_role("ghedit", ghedit_role)
    app.add_directive("imgtable", ImageTableDirective)
    app.add_directive("pintable", PinTableDirective)
    return {"version": "1.0.0", "parallel_read_safe": True, "parallel_write_safe": True}