# SPDX-License-Identifier: GPL-3.0-or-later
# vim: noet:ts=4
# (c) Emmanuel Beffara, 2002--2006
"""
Metapost support for Rubber.

The module parses input files for dependencies and does some checkings on
Metapost's log files after the process. Is it enough?
"""

import os, os.path
import re

import logging

msg = logging.getLogger(__name__)
from rubber.util import _, prog_available
import rubber.depend
import rubber.converters.latex

metapost_logfile_limit = 1000000


def check(source, target, context):
    return prog_available('mpost')


re_input = re.compile(r"input\s+(?P<file>[^\s;]+)")
# This is very restrictive, and so is the parsing routine. FIXME?
re_mpext = re.compile("[0-9]+|mpx|log")
re_mpxerr = re.compile("% line (?P<line>[0-9]+) (?P<file>.*)$")


class MPLogCheck(rubber.converters.latex.LogCheck):
    """
    This class adapats the LogCheck class from the main program to the case of
    MetaPost log files, which are very similar to TeX log files.
    """

    def __init__(self, pwd):
        super(MPLogCheck, self).__init__()
        self.pwd = pwd

    def read(self, name):
        """
        The read() method in LogCheck checks that the log is produced by TeX,
        here we check that it is produced by MetaPost.
        """
        with open(name, encoding='utf_8', errors='replace') as file:
            line = file.readline()
            if not line or line.find("This is MetaPost,") == -1:
                return 1
            self.lines = file.readlines()
        return 0

    def continued(self, line):
        """
        Messages in Metapost logs are broken at 79 characters per line, except
        in some cases where there are lines of this length that are not
        continued...
        """
        if len(line) != 79:
            return 0
        return line[-3:] != "..."

    def get_errors(self):
        """
        Parse the Metapost log file for errors. The file has the same form as
        a TeX log file, so the parser for TeX logs is used. The special case
        is that of TeX errors in Metapost labels, which requires parsing
        another TeX log file.
        """
        for err in super(MPLogCheck, self).get_errors():
            if (err["kind"] != "error" or err["text"] != "Unable to make mpx file."):
                yield err
                continue

            # a TeX error was found: parse mpxerr.log

            # TODO: Remove pylint suppression.
            # pylint: disable=no-member
            log = rubber.converter.latex.LogCheck()
            # FIXME this path has no testcase.
            if not log.readlog(os.path.join(self.pwd, "mpxerr.log"), metapost_logfile_limit):
                yield err
                continue

            # read mpxerr.tex to read line numbers from it

            with open(os.path.join(self.pwd, "mpxerr.tex"), encoding='utf_8',
                      errors='replace') as tex_file:
                tex = tex_file.readlines()

            # get the name of the mpxNNN.tex source

            for line in log.lines:
                if line[:2] == "**":
                    tex_src = os.path.join(".", line[2:].strip())
                    break

            for err in log.get_errors():
                if tex_src != err["file"]:
                    # the error is not in a Metapost source
                    yield err
                    continue

                line = int(err["line"])
                for shift in range(1, line + 1):
                    tex_line = tex[line - shift].rstrip()
                    m = re_mpxerr.search(tex_line)
                    if m:
                        err["line"] = int(m.group("line")) + shift - 2
                        err["file"] = os.path.join(self.pwd, m.group("file"))
                        err["pkg"] = "TeX"
                        yield err
                        break

                if shift == line:
                    # the error is in some verbatimtex
                    yield err


class Dep(rubber.depend.Node):
    """
    This class represents dependency nodes for MetaPost figures. The __init__
    method simply creates one node for the figures and one leaf node for all
    sources.
    """

    def __init__(self, env, target, source):
        self.cmd_pwd = os.path.dirname(source)
        super().__init__()
        self.add_product(target)
        self.include(os.path.basename(source))
        msg.debug(_("%s is made from %s"), target, " ".join(self.sources))
        self.base = source[:-3]
        self.cmd = ["mpost", "\\batchmode;input %s" % os.path.basename(self.base)]
        path = ':'.join(env.path)
        self.penv = {
            "TEXINPUTS": "%s:%s" % (path, os.getenv("TEXINPUTS", "")),
            "MPINPUTS": "%s:%s" % (path, os.getenv("MPINPUTS", ""))
        }
        self.log = None

    def include(self, source):
        """
        This function tries to find a specified MetaPost source (currently all
        in the same directory), appends its actual name to the list, and
        parses it to find recursively included files.
        """
        file = os.path.normpath(os.path.join(self.cmd_pwd, source))
        if os.path.exists(file + ".mp"):
            file = file + ".mp"
        elif not os.path.exists(file):
            return
        self.add_source(file)
        # In case UnicodeDecodeError, at worst a .mp file will
        # not be found.
        with open(file, encoding='utf_8', errors='replace') as fd:
            for line in fd:
                m = re_input.search(line)
                if m:
                    self.include(m.group("file"))

    def run(self):
        """
        Run Metapost from the source file's directory, so that figures are put
        next to their source file.
        """
        msg.info(_("running Metapost on %s"), self.base + ".mp")
        if rubber.util.execute(self.cmd, env=self.penv, pwd=self.cmd_pwd) == 0:
            return True

        # This creates a log file that has the same aspect as TeX logs.

        self.log = MPLogCheck(self.cmd_pwd)
        if not self.log.readlog(self.base + ".log", metapost_logfile_limit):
            msg.error(_(
                "I can't read MetaPost's log file, this is wrong."))
            return False
        return not self.log.errors()

    def get_errors(self):
        """
        Report the errors from the last compilation.
        """
        return self.log.get_errors()

    def clean(self):
        """
        This method removes all the files that the Metapost compilation may
        have created. It is required because the compilation may produce more
        files than just the figures used by the document.
        """
        base = self.base + "."
        ln = len(base)
        dir = os.path.dirname(base)
        if dir == "":
            list = os.listdir(".")
        else:
            list = os.listdir(dir)
        for f in list:
            file = os.path.join(dir, f)
            if file[:ln] == base:
                ext = file[ln:]
                m = re_mpext.match(ext)
                if m and ext[m.end():] == "":
                    msg.info(_("removing %s"), file)
                    os.remove(file)


# The `files' dictionary associates dependency nodes to MetaPost sources. It
# is used to detect when several figures from the same source are included. It
# uses a global variable, and this is authentically BAD. Therefore it deserves
# a big fat huge FIXME. Graphics modules should proably be encapsuled in
# classes the same way as standard Rubber modules, that would help for this
# problem.

files = {}


def convert(source, target, context, env):
    if source in files:
        dep = files[source]
        dep.add_product(target)
    else:
        dep = Dep(env, target, source)
        files[source] = dep
    return dep
