# -*- coding: Latin-1 -*-
"""Graphviz's dot language parser.

The dotparser parses graphviz files in dot and dot files and transforms them
into a class representation defined by pydot.

The module needs pyparsing (tested with version 1.2.2) and pydot (tested with 0.9.10)

Author: Michael Krause <michael@krause-software.de>
"""

__author__ = 'Michael Krause'
__license__ = 'MIT'

import sys
import glob
import pydot
import re

from pyparsing import __version__ as pyparsing_version
from pyparsing import Literal, CaselessLiteral, Word,	\
	Upcase, OneOrMore, ZeroOrMore, Forward, NotAny,		\
	delimitedList, oneOf, Group, Optional, Combine,		\
	alphas, nums, restOfLine, cStyleComment, nums,		\
	alphanums, printables, empty, quotedString,			\
	ParseException, ParseResults, CharsNotIn, _noncomma,\
	dblQuotedString


class P_AttrList:
	def __init__(self, toks):
		self.attrs = {}
		i = 0
		while i < len(toks):
			attrname = toks[i]
			attrvalue = toks[i+1]
			self.attrs[attrname] = attrvalue
			i += 2

	def __repr__(self):
		return "%s(%r)" % (self.__class__.__name__, self.attrs)


class DefaultStatement(P_AttrList):
	def __init__(self, default_type, attrs):
		self.default_type = default_type
		self.attrs = attrs

	def __repr__(self):
		return "%s(%s, %r)" %	\
			(self.__class__.__name__, self.default_type, self.attrs)


def push_top_graph_stmt(str, loc, toks):
	attrs = {}
	g = None
	
	for element in toks:
		if 	isinstance(element, ParseResults) or	\
			isinstance(element, tuple) or			\
			isinstance(element, list):
			
			element = element[0]

		if element == 'strict':
			attrs['strict'] = True
		elif element in ['graph', 'digraph']:
			attrs['graph_type'] = element
		elif type(element) == type(''):
			attrs['graph_name'] = element
		elif isinstance(element, pydot.Graph):
			g = pydot.Graph(**attrs)
			g.__dict__.update(element.__dict__)
			for e in g.get_edge_list():
				e.parent_graph = g
			for e in g.get_node_list():
				e.parent_graph = g
			for e in g.get_subgraph_list():
				e.set_graph_parent(g)

		elif isinstance(element, P_AttrList):
			attrs.update(element.attrs)
		else:
			raise ValueError, "Unknown element statement: %r " % element
	
	if g is not None:
		g.__dict__.update(attrs)
		return g


def add_defaults(element, defaults):
	d = element.__dict__
	for key, value in defaults.items():
		if not d.get(key):
			d[key] = value


def add_elements(g, toks, defaults_graph=None, defaults_node=None, defaults_edge=None):
	
	if defaults_graph is None:
		defaults_graph = {}
	if defaults_node is None:
		defaults_node = {}
	if defaults_edge is None:
		defaults_edge = {}
		
	for element in toks:
		if isinstance(element, pydot.Graph):
			add_defaults(element, defaults_graph)
			g.add_subgraph(element)
		elif isinstance(element, pydot.Node):
			add_defaults(element, defaults_node)
			g.add_node(element)
		elif isinstance(element, pydot.Edge):
			add_defaults(element, defaults_edge)
			g.add_edge(element)
		elif isinstance(element, ParseResults):
			for e in element:
				add_elements(g, [e], defaults_graph, defaults_node, defaults_edge)
		elif isinstance(element, DefaultStatement):
			if element.default_type == 'graph':
				default_graph_attrs = pydot.Node('graph')
				default_graph_attrs.__dict__.update(element.attrs)
				g.add_node(default_graph_attrs)
				defaults_graph.update(element.attrs)
				g.__dict__.update(element.attrs)
			elif element.default_type == 'node':
				default_node_attrs = pydot.Node('node')
				default_node_attrs.__dict__.update(element.attrs)
				g.add_node(default_node_attrs)
# 				defaults_node.update(element.attrs)
			elif element.default_type == 'edge':
				default_edge_attrs = pydot.Node('edge')
				default_edge_attrs.__dict__.update(element.attrs)
				g.add_node(default_edge_attrs)
				defaults_edge.update(element.attrs)
			else:
				raise ValueError, "Unknown DefaultStatement: %s " % element.default_type
		elif isinstance(element, P_AttrList):
			g.__dict__.update(element.attrs)
		else:
			raise ValueError, "Unknown element statement: %r " % element


def push_graph_stmt(str, loc, toks):
	g = pydot.Subgraph()
	add_elements(g, toks)
	return g


def push_subgraph_stmt(str, loc, toks):	

	for e in toks:
		if len(e)==3:
			g = e[2]
			g.set_name(e[1])
		if len(e)==1:
			e[0].set_name('')
			return e[0]

	return g


def push_default_stmt(str, loc, toks):
	# The pydot class instances should be marked as
	# default statements to be inherited by actual
	# graphs, nodes and edges.
	# print "push_default_stmt", toks
	default_type = toks[0][0]
	if len(toks) > 1:
		attrs = toks[1].attrs
	else:
		attrs = {}

	if default_type in ['graph', 'node', 'edge']:
		return DefaultStatement(default_type, attrs)
	else:
		raise ValueError, "Unknown default statement: %r " % toks


def push_attr_list(str, loc, toks):
	p = P_AttrList(toks)
	return p


def get_port(node):

	if len(node)>1:
		if isinstance(node[1], ParseResults):
			if len(node[1][0])==2:
				if node[1][0][0]==':':
					return node[1][0][1]
					
	return None

	
def do_node_ports(n_prev, n_next):
	port = get_port(n_prev)
	if port is not None:
		n_prev_port = ':'+port
	else:
		n_prev_port = ''
		
	port = get_port(n_next)
	if port is not None:
		n_next_port = ':'+port
	else:
		n_next_port = ''
		
	return (n_prev_port, n_next_port)

	
def push_edge_stmt(str, loc, toks):
	
	tok_attrs = [a for a in toks if isinstance(a, P_AttrList)]
	attrs = {}
	for a in tok_attrs:
		attrs.update(a.attrs)
	
	e = []
	n_prev = toks[0]
	if isinstance(toks[2][0], pydot.Graph):
		n_next_list = [[n.get_name(),] for n in toks[2][0].get_node_list()]
		for n_next in [n for n in n_next_list]:
			n_prev_port, n_next_port = do_node_ports(n_prev, n_next)
			e.append(pydot.Edge(n_prev[0]+n_prev_port, n_next[0]+n_next_port, **attrs))
	else:		
		for n_next in [n for n in tuple(toks)[2::2]]:
			n_prev_port, n_next_port = do_node_ports(n_prev, n_next)
			e.append(pydot.Edge(n_prev[0]+n_prev_port, n_next[0]+n_next_port, **attrs))
			n_prev = n_next
		
	return e


def push_node_stmt(str, loc, toks):

	if len(toks) == 2:
		attrs = toks[1].attrs
	else:
		attrs = {}
		
	node_name = toks[0]
	if isinstance(node_name, list) or isinstance(node_name, tuple):
		if len(node_name)>0:
			node_name = node_name[0]
	
	n = pydot.Node('"'+node_name+'"', **attrs)
	return n


def strip_quotes( s, l, t ):
	return [ t[0].strip('"') ]


graphparser = None
def graph_definition():
	global graphparser
	
	if not graphparser:
		# punctuation
		colon  = Literal(":")
		lbrace = Literal("{")
		rbrace = Literal("}")
		lbrack = Literal("[")
		rbrack = Literal("]")
		lparen = Literal("(")
		rparen = Literal(")")
		equals = Literal("=")
		comma  = Literal(",")
		dot    = Literal(".")
		slash  = Literal("/")
		bslash = Literal("\\")
		star   = Literal("*")
		semi   = Literal(";")
		at     = Literal("@")
		minus  = Literal("-")
		
		# keywords
		strict_    = Literal("strict")
		graph_     = Literal("graph")
		digraph_   = Literal("digraph")
		subgraph_  = Literal("subgraph")
		node_      = Literal("node")
		edge_      = Literal("edge")

		
		# token definitions
		
		identifier = Word(alphanums + "_" ).setName("identifier")
		
 		double_quoted_string = dblQuotedString

		alphastring_ = OneOrMore(CharsNotIn(_noncomma))

		ID = (identifier | double_quoted_string.setParseAction(strip_quotes) |\
			alphastring_).setName("ID")
			
		html_text = Combine(Literal("<<") + OneOrMore(CharsNotIn(",]")))
		
		float_number = Combine(Optional(minus) +	\
			OneOrMore(Word(nums + "."))).setName("float_number")
			
		righthand_id =  (float_number | ID | html_text).setName("righthand_id")

		port_angle = (at + ID).setName("port_angle")
		
		port_location = (Group(colon + ID) |	\
			Group(colon + lparen + ID + comma + ID + rparen)).setName("port_location")
			
		port = (Group(port_location + Optional(port_angle)) |	\
			Group(port_angle + Optional(port_location))).setName("port")
			
		node_id = (ID + Optional(port))
		a_list = OneOrMore(ID + Optional(equals.suppress() + righthand_id) +	\
			Optional(comma.suppress())).setName("a_list")
			
		attr_list = OneOrMore(lbrack.suppress() + Optional(a_list) +	\
			rbrack.suppress()).setName("attr_list")
			
		attr_stmt = (Group(graph_ | node_ | edge_) + attr_list).setName("attr_stmt")

		edgeop = (Literal("--") | Literal("->")).setName("edgeop")

		stmt_list = Forward()
		graph_stmt = Group(lbrace.suppress() + Optional(stmt_list) +	\
			rbrace.suppress()).setName("graph_stmt")
			
		subgraph = (Group(Optional(subgraph_ + Optional(ID)) + graph_stmt) |	\
			Group(subgraph_ + ID)).setName("subgraph")
			
		edgeRHS = OneOrMore(edgeop + Group(node_id | subgraph))
		
		edge_stmt = Group(node_id | subgraph) + edgeRHS + Optional(attr_list)

		node_stmt = (node_id + Optional(attr_list) + Optional(semi.suppress())).setName("node_stmt")
		
		assignment = (ID + equals.suppress() + righthand_id).setName("assignment")
		stmt =  (assignment | edge_stmt | attr_stmt | subgraph | node_stmt).setName("stmt")
		stmt_list << OneOrMore(stmt + Optional(semi.suppress()))

		graphparser = (Optional(strict_) + Group((graph_ | digraph_)) +	\
			Optional(ID) + graph_stmt).setResultsName("graph")

		singleLineComment = "//" + restOfLine
		
		
		# actions
		
		graphparser.ignore(singleLineComment)
		graphparser.ignore(cStyleComment)

		assignment.setParseAction(push_attr_list)
		a_list.setParseAction(push_attr_list)
		edge_stmt.setParseAction(push_edge_stmt)
		node_stmt.setParseAction(push_node_stmt)
		attr_stmt.setParseAction(push_default_stmt)

		subgraph.setParseAction(push_subgraph_stmt)
		graph_stmt.setParseAction(push_graph_stmt)
		graphparser.setParseAction(push_top_graph_stmt)

		
	return graphparser


def parse_dot_data(data):
	try:
		data = data.replace('\\\n', '')
		graphparser = graph_definition()
		if pyparsing_version >= '1.2':
			graphparser.parseWithTabs()
		tokens = graphparser.parseString(data)
		graph = tokens.graph
		return graph
	except ParseException, err:
		print err.line
		print " "*(err.column-1) + "^"
		print err
		return None
