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

    @copyright: 2008 Wayne Tucker
    @license: GNU GPL, see COPYING for details.
"""

"""
    BASIC USAGE:

    embed a visualization of a graph in a wiki page:

{{{#!graphviz
digraph G {
    A -> B;
};
}}}

    ADVANCED USAGE:

    This parser will check the first lines of the Graphviz data for C++ style
    comments instructing it to use a different filter (dot, neato, twopi,
    circo, or fdp - see http://graphviz.org/ for more info), use a different
    format for the output (see the FORMATS list in the Parser class below),
    or to generate and pass a client-side image map.

    Options:
      filter - the filter to use (see Parser.FILTERS)
      format - the output format (see Parser.FORMATS)
      cmapx - the map name to use for the client-side image map.  Must match
              the graph name in the graph definition and shouldn't conflict
              with any other graphs that are used on the same page.

    embed a visualization of a graph in a wiki page, using the dot filter and
    providing a client-side image map (the filter=dot and format=png options are
    redundant since those are the defaults for this parser):

{{{#!graphviz
//filter=dot
//format=png
//cmapx=DocumentationMap
digraph DocumentationMap {
    FrontPage [href="FrontPage", root=true];
    HelpOnEditing [href="HelpOnEditing"];
    SyntaxReference [href="SyntaxReference"];
    WikiSandBox [href="WikiSandBox", color="grey"];
    MoinMoin [href="http://moinmo.in"];
    FrontPage -> WikiSandBox;
    FrontPage -> MoinMoin;
    WikiSandBox -> HelpOnEditing;
    WikiSandBox -> SyntaxReference;
    SyntaxReference -> FrontPage;
};
}}}


    KNOWN BUGS:
      - Hasn't been thoroughly checked for potential methods of injecting
        arbitrary HTML into the output.
      - Only compatible with HTML rendering
      - May not use all of the MoinMoin interfaces properly - this is a
        quick hack based on looking at an example and digging through the
        MoinMoin source.  The MoinMoin development docs haven't been
        consulted (yet).
      - Only image formats (png and gif) are currently implemented
      - Comments must start at the beginning of the graphviz block, and at the
        beginning of their respective lines.  They must also not contain
        any extra whitespace surrounding the = sign.

"""

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

BINARY_PATH = '/usr/bin'

import os
import sys
import base64
import string
import exceptions
import codecs
import subprocess
import time
import sha

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

logging = log.getLogger(__name__)

class GraphVizError(exceptions.RuntimeError):
    pass


Dependencies = []

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

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

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

    def format(self, formatter):
        """ Send the text. """
        self.request.flush() # to identify error text

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

        raw_lines = self.raw.splitlines()
        for l in raw_lines:
            if not l[0:2] == '//':
                break
            if l.lower().startswith('//filter='):
                tmp = l.split('=', 1)[1].lower()
                if tmp in Parser.FILTERS:
                    self.filter = tmp
                else:
                    logging.warn('unknown filter %s' % tmp)
            elif l.lower().startswith('//format='):
                tmp = l.split('=', 1)[1]
                if tmp in Parser.FORMATS:
                    self.format = tmp
            elif l.lower().startswith('//cmapx='):
                self.cmapx = wikiutil.escape(l.split('=', 1)[1])

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

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

        img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format)

        self.pagename = formatter.page.page_name
        url = AttachFile.getAttachUrl(self.pagename, img_name, self.request)
        self.attach_dir=AttachFile.getAttachDir(self.request,self.pagename,create=1)

        self.delete_old_graphs(formatter)

        if not os.path.isfile(self.attach_dir + '/' + img_name):
            self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name))

        if self.format in Parser.IMAGE_FORMATS:
            if self.cmapx:
                self.request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n')
                self.request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx))
            else:
                self.request.write(formatter.image(src="%s" % url, alt="graphviz image"))
        else:
            # TODO: read the docs and figure out how to do this correctly
            self.request.write(formatter.attachment_link(True, url=url))

    def delete_old_graphs(self, formatter):
        page_info = formatter.page.lastEditInfo()
        try:
            page_date = page_info['time']
        except exceptions.KeyError, ex:
            return
        attach_files = AttachFile._get_files(self.request, self.pagename)
        for chart in attach_files:
            if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS:
                fullpath = os.path.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)
            else :
                continue

    def graphviz(self, graph_def, fn=None, format=None):
        if not format:
            format = self.format
        if fn:
            p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \
                                 stdin=subprocess.PIPE, \
                                 stderr=subprocess.PIPE)
        else:
            p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.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()

        if not fn:
            output = p.stdout.read()

        errors = p.stderr.read()
        if len(errors) > 0:
            raise GraphVizError, errors

        p = None

        if fn:
            return None
        else:
            return output

