import os
import posixpath
import subprocess
import sys
from os import path
from pathlib import Path
from subprocess import CalledProcessError, PIPE
from typing import Any, Dict, List, Tuple

import sphinx
from diagrams import Diagram
from docutils import nodes
from docutils.nodes import Node
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinx.errors import SphinxError
from sphinx.locale import __
from sphinx.util import logging, sha1
from sphinx.util.docutils import SphinxDirective, SphinxTranslator
from sphinx.util.i18n import search_image_for_language
from sphinx.util.osutil import ensuredir
from sphinx.writers.html import HTMLTranslator

OPTION_FILENAME = "filename"

logger = logging.getLogger(__name__)


class DiagramsError(SphinxError):
    category = "Diagrams error"


class Diagrams(SphinxDirective):
    """
    Directive to insert arbitrary python markup.
    """

    has_content = True
    required_arguments = 0
    optional_arguments = 1
    final_argument_whitespace = False
    option_spec = {"filename": directives.unchanged_required}

    def run(self) -> List[Node]:
        document = self.state.document
        if self.arguments:
            if self.content:
                return [
                    document.reporter.warning(
                        __("Only explicit path supported"), line=self.lineno
                    )
                ]
            argument = search_image_for_language(self.arguments[0], self.env)
            rel_filename, filename = self.env.relfn2path(argument)
            self.env.note_dependency(rel_filename)
            try:
                with open(filename, encoding="utf-8") as fp:
                    diagram_code = fp.read()
            except OSError:
                return [
                    document.reporter.warning(
                        __(
                            "External Diagrams file %r not found or reading "
                            "it failed"
                        )
                        % filename,
                        line=self.lineno,
                    )
                ]
        else:
            diagram_code = "\n".join(self.content)
            filename = self.env.docname + sha1(diagram_code.encode("utf-8")).hexdigest()
            if not diagram_code.strip():
                return [
                    self.state_machine.reporter.warning(
                        __('Ignoring "diagrams" directive without content.'),
                        line=self.lineno,
                    )
                ]
        node = diagrams()
        node["code"] = diagram_code
        node["options"] = {"docname": self.env.docname}
        if OPTION_FILENAME not in self.options:
            inferred_filename = f"{Path(filename).stem}.png"
            node["options"][OPTION_FILENAME] = inferred_filename
            document.reporter.info(
                __(
                    ":filename: argument not pass assuming "
                    f"the output file is named.'{inferred_filename}'"
                ),
                line=self.lineno,
            )
        else:
            node["options"][OPTION_FILENAME] = self.options[OPTION_FILENAME]

        return [node]


class diagrams(nodes.General, nodes.Inline, nodes.Element):
    pass


def render_diagrams(
    self: SphinxTranslator, code: str, options: Dict, prefix: str = "diagrams"
) -> Tuple[str, str]:
    fname: str = options[OPTION_FILENAME]
    if not fname.endswith(".png"):
        fname += ".png"

    relfn = posixpath.join(self.builder.imgpath, fname)
    cwd = path.join(self.builder.outdir, self.builder.imagedir)
    output_filename = path.join(cwd, fname)

    if path.isfile(output_filename):
        return relfn, output_filename

    ensuredir(path.dirname(output_filename))

    python_args = ["python", "-", Path(fname).stem, "false"]

    env = os.environ.copy()
    env["PYTHONPATH"] = f"{os.getcwd()}:{env.get('PYTHONPATH', '')}"

    try:
        ret = subprocess.run(
            python_args,
            input=code.encode(),
            stdout=PIPE,
            stderr=PIPE,
            cwd=cwd,
            env=env,
            check=True,
        )
        if not path.isfile(output_filename):
            raise DiagramsError(
                __(
                    "The diagram in '%r' did not produce an output file: '%r'. "
                    "Ensure the diagram filename is matching the one generated by your code.\n[stderr]\n%r\n"
                    "[stdout]\n%r"
                )
                % (options["docname"], output_filename, ret.stderr, ret.stdout)
            )
        return relfn, output_filename
    except OSError:
        logger.warning(__("The diagram python code could not be run."))
        if not hasattr(self.builder, "_diagrams_warned"):
            self.builder._diagrams_warned = {}  # type: ignore
        self.builder._diagrams_warned["python"] = True  # type: ignore
        return None, None
    except CalledProcessError as exc:
        raise DiagramsError(
            __("python exited with error:\n[stderr]\n%r\n" "[stdout]\n%r")
            % (exc.stderr, exc.stdout)
        )


def render_html(
    self: HTMLTranslator,
    node: diagrams,
    code: str,
    options: Dict,
    prefix: str = "diagrams",
    imgcls: str = None,
) -> None:
    try:
        fname, outfn = render_diagrams(self, code, options, prefix)
    except DiagramsError as exc:
        logger.warning(__("python code %r: %s"), code, exc)
        raise nodes.SkipNode

    if imgcls:
        imgcls += " diagrams"
    else:
        imgcls = "diagrams"

        if "align" in node:
            self.body.append(
                '<div align="%s" class="align-%s">' % (node["align"], node["align"])
            )
        self.body.append('<div class="graphviz">')
        self.body.append(
        '<a href="%s"><img src="%s" class="%s" /></a>'
        % (fname, fname, imgcls)
        )
        self.body.append("</div>\n")
        if "align" in node:
            self.body.append("</div>\n")

    raise nodes.SkipNode


def html_visit_diagrams(self: HTMLTranslator, node: diagrams) -> None:
    render_html(self, node, node["code"], node["options"])


def setup(app: Sphinx) -> Dict[str, Any]:
    app.add_node(diagrams, html=(html_visit_diagrams, None))
    app.add_directive("diagrams", Diagrams)
    return {"version": sphinx.__display_version__, "parallel_read_safe": True}


class SphinxDiagram:
    def __init__(self, argv: List[str] = sys.argv, **kwargs: Any):
        filename = argv[1] if len(argv) >= 2 else f"{Path(argv[0]).stem}"
        show = argv[2].lower() in {"True", "true"} if len(argv) >= 3 else True

        if "title" in kwargs:
            title = kwargs["title"]
            kwargs.pop("title")
        else:

            title = (
                filename.replace("diagram", "")
                .replace("_", " ")
                .replace("-", " ")
                .replace("  ", " ")
                .title()
            )

        _kwargs = {**kwargs, "show": show, "filename": filename}
        self.diagram = Diagram(title, **_kwargs)

    def __exit__(self, *args, **kwargs):
        self.diagram.__exit__(*args, **kwargs)

    def __enter__(self):
        return self.diagram.__enter__()
