#!/usr/bin/env python
#
# msc - Generate text message sequence charts
# Copyright (C) 2001 - Tarball <rubens_ramos@yahoo.com>
# (original Perl version)
# Copyright (C) 2005, 2008 - W. Martin Borgert <debacle@debian.org>
# (current 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 3 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, see <http://www.gnu.org/licenses/>.

import optparse
import sys

import pyparsing

options = None

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:", filename
            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():
    version="1.1.2"
    optparser = optparse.OptionParser(
        usage = "Usage: %prog [options] files", version = version)
    optparser.add_option("-c", "--commentwidth", dest="commentwidth",
                         type=int, help="comments tab distance", default=5)
    optparser.add_option("-m", "--messagewidth", dest="messagewidth",
                         type=int, help="width of message column", default=15)
    optparser.add_option("-p", "--pagebreak", dest="pagebreak",
                         action="store_true", default=False,
			 help="add a pagebreak after chart")
    optparser.add_option("-s", "--swapstyles", dest="swapstyles",
                         action="store_true", default=False,
			 help="swap linestyles for functions and messages")
    optparser.add_option("-t", "--taskwidth", dest="taskwidth", default=8,
                         type=int, help="width of task columns")
    return optparser.parse_args()

if __name__ == "__main__":
    options, 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()
