# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - Graphviz Parser
    Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung

    @copyright: 2008 Wayne Tucker
    @copyright: 2011, 2012 Paul Boddie <paul@boddie.org.uk>
    @license: GNU GPL, see COPYING for details.
"""

__version__ = "0.2"

# Change this to the directory that the Graphviz binaries (dot, neato, etc.)
# are installed in.

BINARY_PATH = '/usr/bin'

from os.path import join
import os
import subprocess
import sha
import re

from MoinMoin import config
from MoinMoin.action import AttachFile
from MoinMoin import log
from MoinMoin import wikiutil

logging = log.getLogger(__name__)

class GraphVizError(RuntimeError):
    pass

Dependencies = ["pages"]

class Parser:

    "Uses the Graphviz programs to create a visualization of a graph."

    extensions = []
    Dependencies = Dependencies

    FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp']
    IMAGE_FORMATS = ['png', 'gif']
    SVG_FORMATS = ['svg', 'svgz']
    OUTPUT_FORMATS = IMAGE_FORMATS + SVG_FORMATS + \
        ['ps', 'fig', 'mif', 'hpgl', 'pcl', 'dia', 'imap']

    attach_regexp = re.compile(
        r"graphviz_"
        r"(?P<digest>.*?)"
        r"(?:"                              # begin optional section
        r"_(?P<width>.*?)_(?P<height>.*?)"  # dimensions
        r")?"                               # end optional section
        r"\.(?P<format>.*)"
        r"$")

    attr_regexp = re.compile(
        r"(?P<attr>width|height)"
        r"\s*=\s*"
        r"""(?P<quote>['"])"""              # start quote
        r"(?P<value>.*?)"
        r"""(?P=quote)""",                  # matching quote
        re.UNICODE)

    def __init__(self, raw, request, **kw):
        self.raw = raw
        self.request = request

    def format(self, formatter):

        "Using the 'formatter', return the formatted page output."

        request = self.request
        page = request.page
        _ = request.getText

        request.flush() # to identify error text

        filter = self.FILTERS[0]
        format = 'png'
        cmapx = None
        width = None
        height = None

        raw_lines = self.raw.splitlines()
        for l in raw_lines:
            if not l[0:2] == '//':
                break

            parts = l[2:].split("=")
            directive = parts[0]
            value = "=".join(parts[1:])

            if directive == 'filter':
                filter = value.lower()
                if filter not in self.FILTERS:
                    logging.warn('unknown filter %s' % filter)

            elif directive == 'format':
                value = value.lower()
                if value in self.OUTPUT_FORMATS:
                    format = value

            elif directive == 'cmapx':
                cmapx = wikiutil.escape(value)

        if not format in self.OUTPUT_FORMATS:
            raise NotImplementedError, "only formats %s are currently supported" % \
                self.OUTPUT_FORMATS

        if cmapx and not format in self.IMAGE_FORMATS:
            logging.warn('format %s is incompatible with cmapx option' % format)
            cmapx = None

        digest = sha.new(self.raw).hexdigest()

        # Make sure that an attachments directory exists and that old graphs are
        # deleted.

        self.attach_dir = AttachFile.getAttachDir(request, page.page_name, create=1)
        self.delete_old_graphs(formatter)

        # Find the details of the graph, rendering a new graph if necessary.

        attrs = self.find_graph(digest, format)
        if not attrs:
            attrs = self.graphviz(filter, self.raw, digest, format)

        chart = self.get_chartname(digest, format, attrs)
        url = AttachFile.getAttachUrl(page.page_name, chart, request)

        # Images are displayed using the HTML "img" element (or equivalent)
        # and may provide an imagemap.

        if format in self.IMAGE_FORMATS:
            if cmapx:
                request.write('\n' + self.graphviz(filter, self.raw, digest, "cmapx") + '\n')
                request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs)))
            else:
                request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs)))

        # Other objects are embedded using the HTML "object" element (or
        # equivalent).

        else:
            request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs)))
            request.write(formatter.text(_("graphviz image")))
            request.write(formatter.transclusion(0))

    def find_graph(self, digest, format):

        "Find an existing graph using 'digest' and 'format'."

        attach_files = AttachFile._get_files(self.request, self.request.page.page_name)

        for chart in attach_files:
            match = self.attach_regexp.match(chart)

            if match and \
                match.group("digest") == digest and \
                match.group("format") == format:

                return match.groupdict()

        return None

    def get_chartname(self, digest, format, attrs=None):

        "Return the chart name for the 'digest', 'format' and 'attrs'."

        wh = self.get_dimensions(attrs)
        if wh:
            dimensions = "_%s_%s" % wh
        else:
            dimensions = ""
        return "graphviz_%s%s.%s" % (digest, dimensions, format)

    def delete_old_graphs(self, formatter):

        "Using the 'formatter' for page information, delete old graphs."

        page_info = formatter.page.lastEditInfo()
        try:
            page_date = page_info['time']
        except KeyError, ex:
            return

        attach_files = AttachFile._get_files(self.request, self.request.page.page_name)

        for chart in attach_files:
            match = self.attach_regexp.match(chart)

            if match and match.group("format") in self.OUTPUT_FORMATS:
                fullpath = join(self.attach_dir, chart).encode(config.charset)
                st = os.stat(fullpath)
                chart_date = self.request.user.getFormattedDateTime(st.st_mtime)
                if chart_date < page_date:
                    os.remove(fullpath)

    def graphviz(self, filter, graph_def, digest, format):

        """
        Using the 'filter' with the given 'graph_def' (and 'digest'), generate
        output in the given 'format'.
        """

        need_output = format in ("cmapx", "svg")

        # Either write the output straight to a file.

        if not need_output:
            chart = self.get_chartname(digest, format)
            filename = join(self.attach_dir, chart).encode(config.charset)

            p = subprocess.Popen([
                join(BINARY_PATH, filter), '-T%s' % format, '-o%s' % filename
                ],
                shell=False,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE)

        # Or intercept the output.

        else:
            p = subprocess.Popen([
                join(BINARY_PATH, filter), '-T%s' % format
                ],
                shell=False,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE)

        p.stdin.write(graph_def)
        p.stdin.flush()
        p.stdin.close()

        p.wait()

        # Graph data always goes via standard output so that we can extract the
        # width and height if possible.

        if need_output:
            output, attrs = self.process_output(p.stdout, format)
        else:
            output, attrs = None, {}

        # Test for errors.

        errors = p.stderr.read()

        if len(errors) > 0:
            raise GraphVizError, errors

        # Return the output for imagemaps.

        if format == "cmapx":
            return output

        # Copy to a file, if necessary.

        elif need_output:
            chart = self.get_chartname(digest, format, attrs)
            filename = join(self.attach_dir, chart).encode(config.charset)

            f = open(filename, "wb")
            try:
                f.write(output)
            finally:
                f.close()

        # Return the dimensions, if defined.

        return attrs

    def process_output(self, output, format):

        "Process graph 'output' in the given 'format'."

        # Return the raw output if SVG is not being produced.

        if format != "svg":
            return output.read(), {}

        # Otherwise, return the processed SVG output.

        processed = []
        found = False
        attrs = {}

        for line in output.xreadlines():
            if not found and line.startswith("<svg "):
                for match in self.attr_regexp.finditer(line):
                    attrs[match.group("attr")] = match.group("value")
                found = True
            processed.append(line)

        return "".join(processed), attrs

    def get_dimensions(self, attrs):

        "Return a (width, height) tuple using the 'attrs' dictionary."

        if attrs and attrs.has_key("width") and attrs.has_key("height"):
            return attrs["width"], attrs["height"]
        else:
            return None

    def get_format_attrs(self, attrs):

        "Return a dictionary based on 'attrs' with only formatting attributes."

        dattrs = {}
        for key in ("width", "height"):
            if attrs.has_key(key):
                dattrs[key] = attrs[key]
        return dattrs

# vim: tabstop=4 expandtab shiftwidth=4
