"""Export to PDF via latex"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

import os
import shutil
import subprocess
import sys
from tempfile import TemporaryDirectory

from traitlets import Bool, Instance, Integer, List, Unicode, default

from nbconvert.utils import _contextlib_chdir

from .latex import LatexExporter


class LatexFailed(IOError):  # noqa
    """Exception for failed latex run

    Captured latex output is in error.output.
    """

    def __init__(self, output):
        """Initialize the error."""
        self.output = output

    def __unicode__(self):
        """Unicode representation."""
        return "PDF creating failed, captured latex output:\n%s" % self.output

    def __str__(self):
        """String representation."""
        u = self.__unicode__()
        return u


def prepend_to_env_search_path(varname, value, envdict):
    """Add value to the environment variable varname in envdict

    e.g. prepend_to_env_search_path('BIBINPUTS', '/home/sally/foo', os.environ)
    """
    if not value:
        return  # Nothing to add

    envdict[varname] = value + os.pathsep + envdict.get(varname, "")


class PDFExporter(LatexExporter):
    """Writer designed to write to PDF files.

    This inherits from `LatexExporter`. It creates a LaTeX file in
    a temporary directory using the template machinery, and then runs LaTeX
    to create a pdf.
    """

    export_from_notebook = "PDF via LaTeX"

    latex_count = Integer(3, help="How many times latex will be called.").tag(config=True)

    latex_command = List(
        ["xelatex", "{filename}", "-quiet"], help="Shell command used to compile latex."
    ).tag(config=True)

    bib_command = List(["bibtex", "{filename}"], help="Shell command used to run bibtex.").tag(
        config=True
    )

    verbose = Bool(False, help="Whether to display the output of latex commands.").tag(config=True)

    texinputs = Unicode(help="texinputs dir. A notebook's directory is added")
    writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={"build_directory": "."})

    output_mimetype = "application/pdf"

    _captured_output = List()

    @default("file_extension")
    def _file_extension_default(self):
        return ".pdf"

    @default("template_extension")
    def _template_extension_default(self):
        return ".tex.j2"

    def run_command(  # noqa
        self, command_list, filename, count, log_function, raise_on_failure=None
    ):
        """Run command_list count times.

        Parameters
        ----------
        command_list : list
            A list of args to provide to Popen. Each element of this
            list will be interpolated with the filename to convert.
        filename : unicode
            The name of the file to convert.
        count : int
            How many times to run the command.
        raise_on_failure: Exception class (default None)
            If provided, will raise the given exception for if an instead of
            returning False on command failure.

        Returns
        -------
        success : bool
            A boolean indicating if the command was successful (True)
            or failed (False).
        """
        command = [c.format(filename=filename) for c in command_list]

        # This will throw a clearer error if the command is not found
        cmd = shutil.which(command_list[0])
        if cmd is None:
            link = "https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex"
            msg = (
                "{formatter} not found on PATH, if you have not installed "
                "{formatter} you may need to do so. Find further instructions "
                "at {link}.".format(formatter=command_list[0], link=link)
            )
            raise OSError(msg)

        times = "time" if count == 1 else "times"
        self.log.info("Running %s %i %s: %s", command_list[0], count, times, command)

        shell = sys.platform == "win32"
        if shell:
            command = subprocess.list2cmdline(command)  # type:ignore
        env = os.environ.copy()
        prepend_to_env_search_path("TEXINPUTS", self.texinputs, env)
        prepend_to_env_search_path("BIBINPUTS", self.texinputs, env)
        prepend_to_env_search_path("BSTINPUTS", self.texinputs, env)

        with open(os.devnull, "rb") as null:
            stdout = subprocess.PIPE if not self.verbose else None
            for _ in range(count):
                p = subprocess.Popen(
                    command,
                    stdout=stdout,
                    stderr=subprocess.STDOUT,
                    stdin=null,
                    shell=shell,  # noqa
                    env=env,
                )
                out, _ = p.communicate()
                if p.returncode:
                    if self.verbose:  # noqa
                        # verbose means I didn't capture stdout with PIPE,
                        # so it's already been displayed and `out` is None.
                        out_str = ""
                    else:
                        out_str = out.decode("utf-8", "replace")
                    log_function(command, out)
                    self._captured_output.append(out_str)
                    if raise_on_failure:
                        msg = f'Failed to run "{command}" command:\n{out_str}'
                        raise raise_on_failure(msg)
                    return False  # failure
        return True  # success

    def run_latex(self, filename, raise_on_failure=LatexFailed):
        """Run xelatex self.latex_count times."""

        def log_error(command, out):
            self.log.critical("%s failed: %s\n%s", command[0], command, out)

        return self.run_command(
            self.latex_command, filename, self.latex_count, log_error, raise_on_failure
        )

    def run_bib(self, filename, raise_on_failure=False):
        """Run bibtex one time."""
        filename = os.path.splitext(filename)[0]

        def log_error(command, out):
            self.log.warning(
                "%s had problems, most likely because there were no citations", command[0]
            )
            self.log.debug("%s output: %s\n%s", command[0], command, out)

        return self.run_command(self.bib_command, filename, 1, log_error, raise_on_failure)

    def from_notebook_node(self, nb, resources=None, **kw):
        """Convert from notebook node."""
        latex, resources = super().from_notebook_node(nb, resources=resources, **kw)
        # set texinputs directory, so that local files will be found
        if resources and resources.get("metadata", {}).get("path"):
            self.texinputs = os.path.abspath(resources["metadata"]["path"])
        else:
            self.texinputs = os.getcwd()

        self._captured_outputs = []
        with TemporaryDirectory() as td, _contextlib_chdir.chdir(td):
            notebook_name = "notebook"
            resources["output_extension"] = ".tex"
            tex_file = self.writer.write(latex, resources, notebook_name=notebook_name)
            self.log.info("Building PDF")
            self.run_latex(tex_file)
            if self.run_bib(tex_file):
                self.run_latex(tex_file)

            pdf_file = notebook_name + ".pdf"
            if not os.path.isfile(pdf_file):
                raise LatexFailed("\n".join(self._captured_output))
            self.log.info("PDF successfully created")
            with open(pdf_file, "rb") as f:
                pdf_data = f.read()

        # convert output extension to pdf
        # the writer above required it to be tex
        resources["output_extension"] = ".pdf"
        # clear figure outputs and attachments, extracted by latex export,
        # so we don't claim to be a multi-file export.
        resources.pop("outputs", None)
        resources.pop("attachments", None)

        return pdf_data, resources
