#!/usr/bin/env python
#
# msc - Generate text message sequence charts
# Copyright (C) 2001 - Tarball <rubens_ramos@yahoo.com> (original Perl version)
# Copyright (C) 2005 - W. Borgert <debacle@debian.org> (new Python version)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

import getopt, sys
import pyparsing

options = {
    'commentwidth': 5, 'help': False, 'messagewidth': 15, 'pagebreak': False,
    'swapstyles': False, 'taskwidth': 8, 'version': "1.1.1" }

class Title:
    def __init__(self, text, filename):
        self.text = text
        self.filename = filename

    def __repr__(self):
        return "\n%s - %s\n" % (self.text, self.filename)

class Tasks:
    def __init__(self, tasks):
        self.tasks = tasks

    def __repr__(self):
        taskformat = " " * (options["messagewidth"]) \
                     + "%%-%ds" % options["taskwidth"] * len(self.tasks)
        return taskformat % tuple(self.tasks) + "\n" \
               + taskformat % tuple(["|"] * len(self.tasks))

class Comment:
    def __init__(self, text, tasks):
        self.text = text
        self.tasks = tasks

    def __repr__(self):
        task = (("|" + " " * (options["taskwidth"] - 1)) * self.tasks)[:-1]
        return " " * options["messagewidth"] + task \
               + " " * (options["commentwidth"] - 5) + self.text

class Separator:
    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return self.text

class Message:
    def __init__(self, name, src, dst, text, tasks, msg):
        self.name = name
        if not msg:
            self.name += "()"
        self.src = src
        self.dst = dst
        self.text = text
        self.tasks = tasks
        self.msg = msg

    def __repr__(self):
        style = '='
        if self.msg ^ options["swapstyles"]:
            style = '-'
        src = self.tasks.index(self.src)
        dst = self.tasks.index(self.dst)
        first = min(src, dst)
        last = max(src, dst)
        msgformat = "%%-%ds" % options["messagewidth"]
        line = ""
        for i in range(len(self.tasks)):
            offset = 1
            start = ""
            end = ""
            if dst > src and i + 1 == dst:
                offset = 2
                end = ">"
            elif src >= dst and i == dst:
                offset = 2
                start = "<"
            if i == len(self.tasks) - 1:
                offset += 1
            length = options["taskwidth"] - offset
            if i < first or i >= last:
                line += "|" + start + " " * length + end
            elif i > first and i < last:
                line += "+" + start + style * length + end
            elif i == first and i != last:
                line += "|" + start + style * length + end
        return (msgformat + line + "%s%s") % \
               (self.name, " " * (options["commentwidth"] - 5), self.text)

class End:
    def __init__(self, tasks):
        self.tasks = tasks

    def __repr__(self):
        taskformat = "%%-%ds" % options["taskwidth"]
        pagebreak = "\n\n"
        if options["pagebreak"]:
            pagebreak += chr(12)
        return " " * options["messagewidth"] + (taskformat * self.tasks) \
               % tuple(["|"] * self.tasks) + pagebreak

class MSC:
    def __init__(self, filename):
        self.actions = []
        self.createbnf()
        self.parsefile(filename)

    def createbnf(self):
        pp = pyparsing
        Colon = pp.Literal(":").suppress()
        Identifier = pp.Word(pp.alphas, pp.alphanums + "_")
        Title = pp.Group(
            pp.Literal("Title").suppress() + Colon \
            + pp.restOfLine.setResultsName("text")).setResultsName("title")
        Tasks = pp.Group(
            pp.Literal("Tasks").suppress() + Colon \
            + Identifier.setResultsName("task") + pp.ZeroOrMore(
            pp.Literal(",").suppress() \
            + Identifier.setResultsName("task"))).setResultsName("tasks")
        Message = pp.Group(
            Identifier.setResultsName("name") + Colon \
            + Identifier.setResultsName("src") + Colon \
            + Identifier.setResultsName("dst") + Colon \
            + pp.restOfLine.setResultsName("text")).setResultsName("msg")
        Function = pp.Group(
            Identifier.setResultsName("name") \
            + pp.Literal("()").suppress() + Colon \
            + Identifier.setResultsName("src") + Colon \
            + Identifier.setResultsName("dst") + Colon \
            + pp.restOfLine.setResultsName("text")).setResultsName("func")
        Separator = pp.Group(
            pp.Literal("*").suppress() \
            + pp.restOfLine.setResultsName("text")).setResultsName("sep")
        Comment = pp.Group(
            Colon + Colon + Colon \
            + pp.restOfLine.setResultsName("text")).setResultsName("cmt")
        Action = (Message ^ Function ^ Separator ^ Comment)
        self.bnf = (Title + Tasks + pp.OneOrMore(Action) \
               + pp.StringEnd()).setResultsName("msc")
        self.bnf.ignore(pp.Literal("#") + pp.restOfLine)

    def appendaction(self, tok, tasks, msg):
        for t in [1, 2]:
            if tok[t] not in tasks:
                raise ValueError, "Couldn't find task %s." % tok[t]
        self.actions.append(
            Message(tok[0], tok[1], tok[2], tok[3], tasks, msg))

    def parsefile(self, filename):
        try:
            tokens = self.bnf.parseFile(filename)
        except pyparsing.ParseException, err:
            print "File:", f
            print err.line
            print " "*(err.column-1) + "^"
            print err
        tasks = []
        for tok in tokens:
            name = tok.getName()
            if name == 'title':
                self.actions.append(Title(tok[0], filename))
            elif name == 'tasks':
                for task in tok:
                    if task in tasks:
                        raise ValueError, "Task %s duplicated." % task
                    tasks.append(task)
                self.actions.append(Tasks(tasks))
            elif name == 'msg':
                self.appendaction(tok, tasks, True)
            elif name == 'func':
                self.appendaction(tok, tasks, False)
            elif name == 'cmt':
                self.actions.append(Comment(tok[0], len(tasks)))
            elif name == 'sep':
                self.actions.append(Separator(tok[0]))
        self.actions.append(End(len(tasks)))

    def printme(self):
        for action in self.actions[:-1]:
            print action
        print "%s" % self.actions[-1:][0],

def parseopts():
    try:
        opts, args = getopt.getopt(
            sys.argv[1:], "c:hm:pst:vx",
            ["commentwidth=", "help", "messagewidth=", "pagebreak",
             "swapstyles", "taskwidth=", "version" ])
    except getopt.GetoptError:
        usage(sys.argv[0])
        sys.exit(1)
    for o, a in opts:
        if o in ("-c", "--commentwidth"):
            options["commentwidth"] = int(a)
        elif o in ("-h", "--help"):
            usage(sys.argv[0])
            sys.exit(0)
        elif o in ("-m", "--messagewidth"):
            options["messagewidth"] = int(a)
        elif o in ("-p", "--pagebreak"):
            options["pagebreak"] = True
        elif o in ("-s", "--swapstyles"):
            options["swapstyles"] = True
        elif o in ("-t", "--taskwidth"):
            options["taskwidth"] = int(a)
        elif o in ("-v", "--version"):
            print options['version']
            sys.exit(0)
    return args

def usage(prog):
    print """msc version %s

Usage:
%s [options] [file...]

Options:
-c|--commentwidth=<int> : comments tab distance
-h|--help : this message
-m|--messagewidth=<int> : width of message column
-p|--pagebreak : add a pagebreak after chart
-s|--swapstyles : swap linestyles for functions and messages
-t|--taskwidth=<int> : width of task columns
-v|--version : msc version number
""" % (options['version'], prog)

if __name__ == "__main__":
    filenames = parseopts()
    for filename in filenames:
        try:
            msc = MSC(filename)
        except ValueError, e:
            print >>sys.stderr, "msc: %s.aborting." % e
            sys.exit(-1)
        msc.printme()
