# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - Parser to create Simple ASCII UML Diagrams 

    @copyright: 1998-2004 Senya Basin (MoinMoin:SB), 
                2007 MoinMoin:ThomasWaldmann 
    @license: GNU GPL, see COPYING for details.
"""

import re
import logging

USAGE = """ UML2ASCII Usage:
   {{{#!uml2ascii [Options]
      Diagram DDL
   }}}
----------------------------------------------------------- 
   DDL Sample:
       [Box 1] -> [Box2] -(3,1)- [Box 3]
       [Box 4|...two liner...] -> --(2,)-- [Box 5.hvc=///]

   Box format:   [pos_x, pos_y, text, attr]
    pos_x      - column position of the box (optional, auto-incremented by default)
    pos_y      - row position of the box (optional, auto-incremented with new-line by default)
    text       - text to display in the box. Required. Use "|" for multi-line text. Use "|-|" for divider.
    attr       - Optional attributes:
                 hvc=... : use alternative characters: Horizontal, Vertical and Corner.
    Box samples: [Box1] [Class Point|-|int getX()|int getY()|void setX(int)|void setY(int)] [1,5,Box at cell 1:5]

   Link format:  START_CHAR -* FROM_TO -* END_CHAR
    START_CHAR - starting character. Required. 
    END_CHAR   - ending character. Required. 
    FROM_TO    - (id_from, id_to). Optional. From previous to next by default.
    id_from    - Box ID from which the links starts. Optional. Previous box by default.
    id_to      - Box ID to which the link goes. Optional. Next box by default.
    Link samples: -> <-> -- <> -(2,5)-> -(,7)->

   Options:
    fg:color   - diagram color
    dx:value   - minimum horizontal gap
    dy:value   - minimum vertical gap
    nn:        - print box IDs
"""

H_CHAR = u'-'
V_CHAR = u'|'
C_CHAR = u'+'

class UMLAsciiDiagram:
    def __init__(self):
        self.pos_x = 0
        self.pos_y = 0
        self.obj_id = 0
        self.tab_nx = 0
        self.tab_ny = 0
        self.boxes = {}
        self.links_from = {}
        self.cols_width = {}
        self.rows_height = {}
        self.cols_start = []
        self.rows_start = []
        self.DEBUG = 0
        self.fgcolor = 'black'
        self.DX = 5
        self.DY = 3
        self.NN = 0

    def parse_params(self, args):
        m = re.match('.*(fgcolor|fg):(\S+).*', args, re.I)
        if m:
            self.fgcolor = m.group(2)

        m = re.match('.*DY:(\d+)\s.*', args, re.I)
        if m:
            self.DY = int(m.group(1))

        m = re.match('.*DX:(\d+)\s.*', args, re.I)
        if m:
            self.DX = int(m.group(1))

        if re.match('.*NN:.*', args, re.I):
            self.NN = 1

        if re.match('.*DEBUG:.*', args, re.I):
            self.DEBUG = 1

    def pdebug(self, *args):
        if self.DEBUG:
            logging.debug(args)

    def parse_ddl(self, args):
        re_box  = re.compile(r'\[(\d*),?(\d*),?"?(.*?)\s*?"?,?(\w*?=.*?)?\]')
        re_link = re.compile(r'(\s?[<-])[-(]*(\d*),?(\d*)[-)]*([->])\s?')

        if re.match('^\s*$', args):
            return 0

        rc = 0
        self.pos_y += 1
        self.pos_x = 0

        for tok in re.findall('\[.*?\]|[<-].*?[->]\s', args):
            #s.pdebug("parsed TOKEN:", tok)
            if re.match('\[\]', tok):
                self.pos_x += 1
                continue
            
            m = re_box.match(tok)
            if m:
                x, y, text, attr = m.group(1, 2, 3, 4)
                self.add_box(x, y, text, attr)
                rc = 1
            
            m = re_link.match(tok)
            if m:
                fr, to, start, end = m.group(2, 3, 1, 4)
                self.add_link(fr, to, start.strip(), end.strip())

        return rc

    def add_box(self, x='', y='', text='', attr='', id=None):
        self.obj_id = parse_int(id, self.obj_id + 1)
        self.pos_x = parse_int(x, self.pos_x + 1)
        self.pos_y = parse_int(y, self.pos_y)

        hvc = ''
        if attr:
            m = re.match('hvc=(...)', attr)    
            if m:
                hvc = m.group(1)
            m = re.match('id=(\d+)', attr)    
            if m:
                self.obj_id = m.group(1)
        
        self.pdebug('BOX: x=', self.pos_x, '; y=', self.pos_y, "; text='",text,"'; hvc=",hvc,"; id=",self.obj_id)

        width, height, lines = make_textbox(text, hvc)
        self.boxes[self.obj_id] = (self.obj_id, self.pos_x, self.pos_y, text, attr, width, height, lines)

        if not self.cols_width.has_key(self.pos_x) or width > self.cols_width[self.pos_x]:
            self.cols_width[self.pos_x] = width

        if not self.rows_height.has_key(self.pos_y) or height > self.rows_height[self.pos_y]:
            self.rows_height[self.pos_y] = height

    def add_link(self, from_id='', to_id='', start=H_CHAR, end=H_CHAR, attr=None):
        from_id = parse_int(from_id, self.obj_id)
        to_id = parse_int(to_id, self.obj_id + 1)

        if not self.links_from.has_key(from_id):
            self.links_from[from_id] = []

        links = self.links_from[from_id]
        links.append([from_id, to_id, start, end, attr])
        self.pdebug ("LINK: from=",from_id," to=",to_id," link=\""+start+end+"\" attr=",attr)

    def define_geometry(self):
        self.tab_nx = len(self.cols_width.keys())
        self.tab_ny = len(self.rows_height.keys())

        for i in range(self.tab_nx + 1):
            self.cols_start.append(0)

        for i in range(2, self.tab_nx + 1):
            self.cols_start[i] = self.cols_start[i-1] + self.cols_width[i-1] + self.DX

        for i in range(self.tab_ny + 1):
            self.rows_start.append(0)

        for i in range(2, self.tab_ny + 1):
            self.rows_start[i] = self.rows_start[i-1] + self.rows_height[i-1] + self.DY

        return self.tab_nx * self.tab_ny

    def draw_box(self, screen, box_id):
        box_id, tx, ty, text, attr, width, height, lines = self.boxes[box_id]
        for j in range(height):
            yy = self.rows_start[ty] + j
            for i in range(width):
                xx = self.cols_start[tx] + i
                screen[yy][xx] = lines[j][i]

        if self.NN:
            sid = repr(box_id)
            for i in range(len(sid)):
                screen[self.rows_start[ty]][self.cols_start[tx] + i] = sid[i]

    def draw_hline (self, screen, x1, x2, y, c_start, c_end):
        step = (x2 >= x1) and 1 or -1
        for x in range(x1, x2, step):
            screen[y][x] = H_CHAR
            
        if c_start:
            if step == -1:
                if c_start == u'<':
                    c_start = u'>'
                start_offs = -1
            else:
                start_offs = 0
            screen[y][x1+start_offs] = c_start
        if c_end:
            if step == -1:
                if c_end == u'>':
                    c_end = u'<'
                end_offs = 0
            else:
                end_offs = -1
            screen[y][x2+end_offs] = c_end

    def draw_h2line (self, screen, x1, x2, y, c_start, c_end):
        self.draw_hline(screen, x1, x2+2, y+1, c_start, c_end)
        screen[y][x1] = V_CHAR
        screen[y+1][x1] = C_CHAR
        screen[y][x2+1] = V_CHAR
        screen[y+1][x2+1] = C_CHAR

    def draw_vline (self, screen, x, y1, y2, c_start=V_CHAR, c_end=V_CHAR):
        step = (y2 >= y1) and 1 or -1
        for y in range(y1, y2, step):
            screen[y][x] = V_CHAR

        if c_start:
            if step >= 0:
                if c_start == u'<':
                    c_start = u'^'
                elif c_start == u'-':
                    c_start = V_CHAR
            else:
                if c_start == u'<':
                    c_start = u'v'
                elif c_start == u'-':
                    c_start = V_CHAR
            screen[y1][x] = c_start
        if c_end:
            if step >= 0:
                if c_end == u'>':
                    c_end = u'v'
                elif c_end == u'-':
                    c_end = V_CHAR
                end_offs = -1
            else:
                if c_end == u'>':
                    c_end = u'^'
                elif c_end == u'-':
                    c_end = V_CHAR
                end_offs = 0
            screen[y2+end_offs][x] = c_end

    def draw_link(self, screen, link):
        fr, to, start, end, attr = link

        box_id1, tx1, ty1, undef, undef, w1, h1, undef = self.boxes[fr]
        if not box_id1:
            return

        box_id2, tx2, ty2, undef, undef, w2, h2, undef = self.boxes[to]
        if not box_id2:
            return

        if ty1 == ty2:
            if abs(tx2 - tx1) == 1:
                if tx1 < tx2:
                    x1 = self.cols_start[tx1] + w1
                    x2 = self.cols_start[tx2]
                else:
                    x1 = self.cols_start[tx1]
                    x2 = self.cols_start[tx2] + w2

                h = min(h1, h2) // 2
                    
                self.draw_hline(screen, x1, x2, self.rows_start[ty1] + h, start, end)
            
            elif abs(tx2 - tx1) > 1:
                if tx1 < tx2:
                    x1 = self.cols_start[tx1] + w1 // 2
                    x2 = self.cols_start[tx2] + w2 // 2 - 1
                else:
                    x1 = self.cols_start[tx2] + w2 // 2
                    x2 = self.cols_start[tx1] + w1 // 2 - 1

                h = max(h1, h2)
                    
                self.draw_h2line(screen, x1, x2, self.rows_start[ty1] + h, start, end)

        elif tx1 == tx2 and abs(ty2 - ty1) == 1:
            if ty1 < ty2:
                y1 = self.rows_start[ty1] + self.rows_height[ty1]
                y2 = self.rows_start[ty2]
            else:
                y1 = self.rows_start[ty2] + self.rows_height[ty2]
                y2 = self.rows_start[ty1]

            w = min(w1, w2) // 2

            self.draw_vline(screen, self.cols_start[tx1] + w, y1, y2, start, end)

        else:
            if tx1 < tx2:
                x1 = self.cols_start[tx1] + w1
                x2 = self.cols_start[tx2]                
                y1 = self.rows_start[ty1] + h1 // 2
                y2 = self.rows_start[ty2] + h2 // 2
                w = self.DX // 2
                self.draw_hline(screen, x1, x1 + w, y1, start, H_CHAR)
                self.draw_vline(screen, x1 + w, y1, y2)
                self.draw_hline(screen, x1 + w, x2, y2, H_CHAR, end)
            else:
                x1 = self.cols_start[tx1] + w1 // 2
                x2 = self.cols_start[tx2] + w2 // 2
                y1 = self.rows_start[ty1] + h1
                y2 = self.rows_start[ty2]
                h = self.DY // 2
                self.draw_vline(screen, x1, y1, y1 + h, start, V_CHAR)
                self.draw_hline(screen, x1, x2, y1 + h, H_CHAR, H_CHAR)
                self.draw_vline(screen, x2, y1 + h, y2, V_CHAR, end)

    def draw_diagram(self):
        screen_w = self.cols_start[self.tab_nx] + self.cols_width[self.tab_nx]
        screen_h = self.rows_start[self.tab_ny] + self.rows_height[self.tab_ny]
        screen = [[u' ' for x in range(screen_w)] for y in range(screen_h)]

        for links in self.links_from.values():
            for link in links:
                self.draw_link(screen, link)

        for box_id in  self.boxes.keys():
            self.draw_box(screen, box_id)

        return self.print_screen(screen)    

    def print_screen(self, screen):
        return u'\n'.join([u''.join(line) for line in screen]) + u'\n'


def make_textbox (text, symbols=''):
    """ split text into lines and render it into a box
        return total width, total height and formatted lines
    """    
    if not symbols:
        symbols = H_CHAR + V_CHAR + C_CHAR
    hchar, vchar, cchar = symbols

    lines = text.split(V_CHAR)
    max_len = max([len(line) for line in lines])

    # delimiter and top/bottom horizontal lines
    hline = u'-' * (max_len + 2)
    top_bottom_line = u"%s%s%s" % (cchar, hline, cchar)
    
    # now render box
    formatted_lines = [top_bottom_line]
    for line in lines:
        if re.match('\s*\-+\s*', line): # delimiter line wanted
            l = hline
        else: # normal content
            l = u" %s " % line.ljust(max_len)
        formatted_lines.append(u"%s%s%s" % (vchar, l, vchar))
    formatted_lines.append(top_bottom_line)

    return max_len + 4, len(formatted_lines), formatted_lines


def parse_int(val, dflt):
    if val:
        return int(val)
    else:
        return int(dflt)


Dependencies = ['justfordebugging']

class Parser:
    """
    """

    ## specify extensions willing to handle (for inline:)
    extensions = ['.uml2ascii', ]
    Dependencies = ['justfordebugging']
    
    def __init__(self, raw, request, **kw):
        self.raw = raw
        self.request = request
        self.form = request.form
        self._ = request.getText

    def format(self, formatter):
        """ Send the text. """
        request = self.request
        lines = self.raw.split('\n')
        ad = UMLAsciiDiagram()
        ad.parse_params(lines[0])
        del lines[0]
        
        rc = 0
        for line in lines:
            rc += ad.parse_ddl(line)

        if rc > 0:
            ad.define_geometry()
            sb = ad.draw_diagram()
        else:
            sb = '\n'.join(lines)
            sb = USAGE + sb

        request.write(formatter.preformatted(1))
        request.write(formatter.rawHTML(sb))
        request.write(formatter.preformatted(0))

