"""
Lexical analizer for attributes and operations.

In this module some parse functions are added for attributes and operations.
The regular expressions are constructed based on a series of
"sub-patterns". This makes it easy to identify the autonomy of an
attribute/operation.
"""

__all__ = [
        'parse_property', 'parse_operation',
        ]

import re
from simplegeneric import generic

#from gaphor.UML import uml2 as UML
import uml2 as UML


@generic
def parse(el, text):
    """
    Parser for an UML element.
    """
    raise NotImplementedError('Parsing routine for type %s not implemented yet' \
            % type(el))


# Visibility (optional) ::= '+' | '-' | '#'
vis_subpat = r'\s*(?P<vis>[-+#])?'

# Derived value (optional) ::= [/]
derived_subpat = r'\s*(?P<derived>/)?'

# name (required) ::= name
name_subpat = r'\s*(?P<name>[a-zA-Z_]\w*)'

# Multiplicity (added to type_subpat) ::= '[' [mult_l ..] mult_u ']'
mult_subpat = r'\s*(\[\s*((?P<mult_l>[0-9]+)\s*\.\.)?\s*(?P<mult_u>([0-9]+|\*))\s*\])?'
multa_subpat = r'\s*(\[?((?P<mult_l>[0-9]+)\s*\.\.)?\s*(?P<mult_u>([0-9]+|\*))\]?)?'

# Type and multiplicity (optional) ::= ':' type [mult]
type_subpat = r'\s*(:\s*(?P<type>\w+)\s*' + mult_subpat + r')?'

# default value (optional) ::= '=' default
default_subpat = r'\s*(=\s*(?P<default>\S+))?'

# tagged values (optional) ::= '{' tags '}'
tags_subpat = r'\s*(\{\s*(?P<tags>.*?)\s*\})?'

# Parameters (required) ::= '(' [params] ')'
params_subpat = r'\s*\(\s*(?P<params>[^)]+)?\)'

# Possible other parameters (optional) ::= ',' rest
rest_subpat = r'\s*(,\s*(?P<rest>.*))?'

# Direction of a parameter (optional, default in) ::= 'in' | 'out' | 'inout'
dir_subpat = r'\s*((?P<dir>in|out|inout)\s)?'

# Some trailing garbage => no valid syntax...
garbage_subpat = r'\s*(?P<garbage>.*)'

def compile(regex):
    return re.compile(regex, re.MULTILINE | re.S)

# Attribute:
#   [+-#] [/] name [: type[\[mult\]]] [= default] [{ tagged values }]
attribute_pat = compile(r'^' + vis_subpat + derived_subpat + name_subpat + type_subpat + default_subpat + tags_subpat + garbage_subpat)

# Association end name:
#   [[+-#] [/] name [\[mult\]]] [{ tagged values }]
association_end_name_pat = compile(r'^' + '(' + vis_subpat + derived_subpat + name_subpat + mult_subpat + ')?' + tags_subpat + garbage_subpat)

# Association end multiplicity:
#   [mult] [{ tagged values }]
association_end_mult_pat = compile(r'^' + multa_subpat + tags_subpat + garbage_subpat)

# Operation:
#   [+|-|#] name ([parameters]) [: type[\[mult\]]] [{ tagged values }]
operation_pat = compile(r'^' + vis_subpat + name_subpat + params_subpat + type_subpat + tags_subpat + garbage_subpat)

# One parameter supplied with an operation:
#   [in|out|inout] name [: type[\[mult\]] [{ tagged values }]
parameter_pat = compile(r'^' + dir_subpat + name_subpat + type_subpat + default_subpat + tags_subpat + rest_subpat)

# Lifeline:
#  [name] [: type]
lifeline_pat = compile('^' + name_subpat + type_subpat + garbage_subpat)

def _set_visibility(el, vis):
    if vis == '+':
        el.visibility = 'public'
    elif vis == '#':
        el.visibility = 'protected'
    elif vis == '~':
        el.visibility = 'package'
    elif vis == '-':
        el.visibility = 'private'
    else:
        try:
            del el.visibility
        except AttributeError:
            pass

def parse_attribute(el, s):
    """
    Parse string s in the property. Tagged values, multiplicity and stuff
    like that is altered to reflect the data in the property string.
    """
    m = attribute_pat.match(s)
    if not m or m.group('garbage'):
        el.name = s
        del el.visibility
        del el.isDerived
        if el.typeValue:
            el.typeValue.value = None
        if el.lowerValue:
            el.lowerValue.value = None
        if el.upperValue:
            el.upperValue.value = None
        if el.defaultValue:
            el.defaultValue.value = None
    else:
        g = m.group
        create = el._factory.create
        _set_visibility(el, g('vis'))
        el.isDerived = g('derived') and True or False
        el.name = g('name')
        if not el.typeValue:
            el.typeValue = create(UML.LiteralSpecification)
        el.typeValue.value = g('type')
        if not el.lowerValue:
            el.lowerValue = create(UML.LiteralSpecification)
        el.lowerValue.value = g('mult_l')
        if not el.upperValue:
            el.upperValue = create(UML.LiteralSpecification)
        el.upperValue.value = g('mult_u')
        if not el.defaultValue:
            el.defaultValue = create(UML.LiteralSpecification)
        el.defaultValue.value = g('default')
        # Skip tags: should do something with stereotypes?
        #tags = g('tags')
        #if tags:
        #    for t in map(str.strip, tags.split(',')):
        #        tv = create(UML.LiteralSpecification)
        #        tv.value = t
        #        el.taggedValue = tv


def parse_association_end(el, s):
    """
    Parse the text at one end of an association. The association end holds
    two strings. It is automattically figured out which string is fed to the
    parser.
    """
    create = el._factory.create

    # if no name, then clear as there could be some garbage
    # due to previous parsing (i.e. '[1'
    m = association_end_name_pat.match(s)
    if m and not m.group('name'):
        el.name = None

    # clear also multiplicity if no characters in ``s``
    m = association_end_mult_pat.match(s)
    if m and not m.group('mult_u'):
        if el.upperValue:
            el.upperValue.value = None

    if m and m.group('mult_u') or m.group('tags'):
        g = m.group
        if not el.lowerValue:
            el.lowerValue = create(UML.LiteralSpecification)
        el.lowerValue.value = g('mult_l')
        if not el.upperValue:
            el.upperValue = create(UML.LiteralSpecification)
        el.upperValue.value = g('mult_u')
        #tags = g('tags')
        #if tags:
        #    for t in map(str.strip, tags.split(',')):
        #        tv = create(UML.LiteralSpecification)
        #        tv.value = t
        #        el.taggedValue = tv
    else:
        m = association_end_name_pat.match(s)
        g = m.group
        if g('garbage'):
            el.name = s
            del el.visibility
            del el.isDerived
        else:
            _set_visibility(el, g('vis'))
            el.isDerived = g('derived') and True or False
            el.name = g('name')
            # Optionally, the multiplicity and tagged values may be defined:
            if g('mult_l'):
                if not el.lowerValue:
                    el.lowerValue = create(UML.LiteralSpecification)
                el.lowerValue.value = g('mult_l')

            if g('mult_u'):
                if not g('mult_l') and el.lowerValue:
                    el.lowerValue.value = None
                if not el.upperValue:
                    el.upperValue = create(UML.LiteralSpecification)
                el.upperValue.value = g('mult_u')

            #tags = g('tags')
            #if tags:
            #    while el.taggedValue:
            #        el.taggedValue[0].unlink()
            #    for t in map(str.strip, tags.split(',')):
            #        tv = create(UML.LiteralSpecification)
            #        tv.value = t
            #        el.taggedValue = tv

@parse.when_type(UML.Property)
def parse_property(el, s):
    if el.association:
        parse_association_end(el, s)
    else:
        parse_attribute(el, s)


@parse.when_type(UML.Operation)
def parse_operation(el, s):
    """
    Parse string s in the operation. Tagged values, parameters and
    visibility is altered to reflect the data in the operation string.
    """
    m = operation_pat.match(s)
    if not m or m.group('garbage'):
        el.name = s
        del el.visibility
        map(UML.Parameter.unlink, list(el.returnResult))
        map(UML.Parameter.unlink, list(el.formalParameter))
    else:
        g = m.group
        create = el._factory.create
        _set_visibility(el, g('vis'))
        el.name = g('name')
        if not el.returnResult:
            el.returnResult = create(UML.Parameter)
        p = el.returnResult[0]
        p.direction = 'return'
        if not p.typeValue:
            p.typeValue = create(UML.LiteralSpecification)
        p.typeValue.value = g('type')
        if not p.lowerValue:
            p.lowerValue = create(UML.LiteralSpecification)
        p.lowerValue.value = g('mult_l')
        if not p.upperValue:
            p.upperValue = create(UML.LiteralSpecification)
        p.upperValue.value = g('mult_u')
        # FIXME: Maybe add to Operation.ownedRule?
        #tags = g('tags')
        #if tags:
        #    for t in map(str.strip, tags.split(',')):
        #        tv = create(UML.LiteralSpecification)
        #        tv.value = t
        #        p.taggedValue = tv
        
        pindex = 0
        params = g('params')
        while params:
            m = parameter_pat.match(params)
            if not m:
                break
            g = m.group
            try:
                p = el.formalParameter[pindex]
            except IndexError:
                p = create(UML.Parameter)
            p.direction = g('dir') or 'in'
            p.name = g('name')
            if not p.typeValue:
                p.typeValue = create(UML.LiteralSpecification)
            p.typeValue.value = g('type')
            if not p.lowerValue:
                p.lowerValue = create(UML.LiteralSpecification)
            p.lowerValue.value = g('mult_l')
            if not p.upperValue:
                p.upperValue = create(UML.LiteralSpecification)
            p.upperValue.value = g('mult_u')
            if not p.defaultValue:
                p.defaultValue = create(UML.LiteralSpecification)
            p.defaultValue.value = g('default')
            #tags = g('tags')
            #if tags:
            #    for t in map(str.strip, tags.split(',')):
            #        tv = create(UML.LiteralSpecification)
            #        tv.value = t
            #        p.taggedValue = tv
            el.formalParameter = p

            # Do the next parameter:
            params = g('rest')
            pindex += 1

        # Remove remaining parameters:
        for fp in el.formalParameter[pindex:]:
            fp.unlink()

def parse_lifeline(el, s):
    """
    Parse string s in a lifeline. If a class is defined and can be found
    in the datamodel, then a class is connected to the lifelines 'represents'
    property.
    """
    m = lifeline_pat.match(s)
    g = m.group
    if not m or g('garbage'):
        el.name = s
        if hasattr(el, 'represents'):
            del el.represents
    else:
        el.name = g('name') + ": "
        t = g('type')
        if t:
            el.name += ': ' + t
        # In the near future the data model should be extended with 
        # Lifeline.represents: ConnectableElement



def render_lifeline(el):
    """
    """
    return el.name


@parse.when_type(UML.NamedElement)
def parse_namedelement(el, text):
    """
    Parse named element by simply assigning text to its name.
    """
    el.name = text


@parse.when_type(UML.LiteralSpecification)
def parse_literalspecification(el, text):
    """
    Parse literal specification element.
    """
    el.value = text


# vim:sw=4:et:ai
