# -*- coding: utf-8 -*-
"""
    MoinMoin - Graphviz Parser
    Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung

    @copyright: 2008 Wayne Tucker
    @copyright: 2011, 2012 Paul Boddie <paul@boddie.org.uk>
    @copyright: 2012 Frederick Capovilla (Libéo) <fcapovilla@live.ca>
    @license: GNU GPL, see COPYING for details.
"""

__version__ = "0.2.3"

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

BINARY_PATH = '/usr/bin/'

from os.path import join
from StringIO import StringIO
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

        # NOTE: Flushing the request is not supported in 1.9.

        if hasattr(request, "flush"):
            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.encode('utf-8')).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)

        (stdoutdata, stderrdata) = p.communicate(input=graph_def.encode('utf-8'))

        # 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(StringIO(stdoutdata), format)
        else:
            output, attrs = None, {}

        # Test for errors.

        errors = stderrdata

        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.readlines():
            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.get("width") and attrs.get("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
