
"""
Semantic Interfaces (SEM-I) describe the inventory of semantic
components in a grammar, include variables, properties, roles, and
predicates. This information can be used for validating semantic
structures or for filling out missing information in incomplete
representations.
"""


import re
from os.path import dirname, join as pjoin
from collections import namedtuple

_SEMI_SECTIONS = (
    'variables',
    'properties',
    'roles',
    'predicates',
)

_variable_entry_re = re.compile(
    r'(?P<var>[^ <:.]+)'
    r'(?: < (?P<supertypes>[^ &:.]+(?: & [^ &:.]+)*))?'
    r'(?: : (?P<properties>[^ ]+ [^ ,.]+(?:, [^ ]+ [^ ,.]+)*))?'
    r'\s*\.\s*$',
    re.U
)

_property_entry_re = re.compile(
    r'(?P<type>[^ <.]+)'
    r'(?: < (?P<supertypes>[^ &.]+(?: & [^ &.]+)*))?'
    r'\s*\.\s*$',
    re.U
)

_role_entry_re = re.compile(
    r'(?P<role>[^ :]+) : (?P<value>[^ .]+)\s*\.\s*$',
    re.U
)

_predicate_entry_re = re.compile(
    r'(?P<pred>[^ <:.]+)'
    r'(?: < (?P<supertypes>[^ &:.]+(?: & [^ &:.]+)*))?'
    r'(?: : (?P<synposis>.*[^ .]))?'
    r'\s*\.\s*$',
    re.U
)

_synopsis_re = re.compile(
    r'\s*(?P<opt>\[\s*)?'
    r'(?P<role>[^ ]+) (?P<value>[^ ,.{\]]+)'
    r'(?:\s*\{\s*(?P<properties>[^ ]+ [^ ,}]+(?:, [^ ]+ [^ ,}]+)*)\s*\})?'
    r'(?(opt)\s*\])'
    r'(?:\s*(?P<end>,|$)\s*)',
    re.U
)

TOP = '*top*'


class Variable(namedtuple('Variable', ('type', 'supertypes', 'proplist'))):
    @property
    def properties(self):
        return dict(self.proplist)

    @classmethod
    def from_dict(cls, d):
        return cls(
            d['type'], tuple(d['parents']), list(d['properties'].items())
        )

    def to_dict(self):
        return {
            'type': self.type,
            'parents': list(self.supertypes),
            'properties': self.properties
        }


class Role(namedtuple('Role', ('rargname', 'value', 'proplist', 'optional'))):
    def __new__(cls, rargname, value, proplist=None, optional=False):
        if proplist is None: proplist = []
        return super(Role, cls).__new__(
            cls, rargname, value, proplist, optional
        )

    @property
    def properties(self):
        return dict(self.proplist)

    @classmethod
    def from_dict(cls, d):
        return cls(
            d['rargname'],
            d['value'],
            list(d.get('properties', {}).items()),
            d.get('optional', False)
        )

    def to_dict(self):
        d = {'rargname': self.rargname, 'value': self.value}
        if self.properties:
            d['properties'] = self.properties
        if self.optional:
            d['optional'] = self.optional
        return d


class Property(namedtuple('Property', ('type', 'supertypes'))):
    @classmethod
    def from_dict(cls, d):
        return cls(d['type'], tuple(d['parents']))

    def to_dict(self):
        return {'type': self.type, 'parents': list(self.supertypes)}


class Predicate(namedtuple('Predicate',
                           ('predicate', 'supertypes', 'synopses'))):
    @classmethod
    def from_dict(cls, d):
        synopses = [tuple(map(Role.from_dict, synopsis))
                    for synopsis in d.get('synopses', [])]
        return cls(d['predicate'], tuple(d['parents']), synopses)

    def to_dict(self):
        return {
            'predicate': self.predicate,
            'parents': list(self.supertypes),
            'synopses': [[role.to_dict() for role in synopsis]
                         for synopsis in self.synopses]
        }

# TOP = type('*top*', tuple(), {})
# _var_dict = {
#     'supertypes': lambda self: self.__bases__,
# }

def load(fn):
    """
    Read the SEM-I beginning at the filename *fn* and return the SemI.

    Args:
        fn: the filename of the top file for the SEM-I. Note: this must
            be a filename and not a file-like object.
    Returns:
        The SemI defined by *fn*
    """
    data = _load_file(fn)
    return SemI(**data)

def _load_file(fn):
    data = _read_file(fn, dirname(fn))
    data['predicates'] = [
        (pred, Predicate(pred, tuple(d['supertypes']), d['synopses']))
        for pred, d in data['predicates'].items()
    ]
    return data

def _read_file(fn, basedir):
    data = {
        'variables': [],
        'properties': [],
        'roles': [],
        'predicates': {},
    }
    section = None

    for line in open(fn, 'r'):
        line = line.lstrip()

        if not line or line.startswith(';'):
            continue

        match = re.match(r'(?P<name>[^: ]+):\s*$', line)
        if match is not None:
            name = match.group('name')
            if name not in _SEMI_SECTIONS:
                raise ValueError('Invalid SEM-I section: {}'.format(name))
            else:
                section = name
            continue

        match = re.match(r'include:\s*(?P<filename>.+)$', line, flags=re.U)
        if match is not None:
            include_fn = pjoin(basedir, match.group('filename').rstrip())
            include_data = _read_file(include_fn, dirname(include_fn))
            data['variables'].extend(include_data.get('variables', []))
            data['properties'].extend(include_data.get('properties', []))
            data['roles'].extend(include_data.get('roles', []))
            for pred, d in include_data['predicates'].items():
                if pred not in data['predicates']:
                    data['predicates'][pred] = {
                        'supertypes': [],
                        'synopses': []
                    }
                if d.get('supertypes'):
                    data['predicates'][pred]['supertypes'] = d['supertypes']
                if d.get('synopses'):
                    data['predicates'][pred]['synopses'].extend(d['synopses'])

        if section == 'variables':
            # e.g. e < i : PERF bool, TENSE tense.
            match = _variable_entry_re.match(line)
            if match is not None:
                identifier = match.group('var')
                supertypes = match.group('supertypes') or tuple()
                if supertypes:
                    supertypes = tuple(supertypes.split(' & '))
                properties = match.group('properties') or []
                if properties:
                    pairs = properties.split(', ')
                    properties = [tuple(pair.split()) for pair in pairs]
                v = Variable(identifier, supertypes, properties)
                # v = type(identifier, supertypes, d)
                data['variables'].append((identifier, v))
            else:
                raise ValueError('Invalid entry: {}'.format(line))

        elif section == 'properties':
            # e.g. + < bool.
            match = _property_entry_re.match(line)
            if match is not None:
                _type = match.group('type')
                supertypes = match.group('supertypes') or tuple()
                if supertypes:
                    supertypes = tuple(supertypes.split(' & '))
                p = Property(_type, supertypes)
                data['properties'].append((_type, p))
            else:
                raise ValueError('Invalid entry: {}'.format(line))

        elif section == 'roles':
            # e.g. + < bool.
            match = _role_entry_re.match(line)
            if match is not None:
                rargname, value = match.group('role'), match.group('value')
                r = Role(rargname, value)
                data['roles'].append((rargname, r))
            else:
                raise ValueError('Invalid entry: {}'.format(line))

        elif section == 'predicates':
            # e.g. _predicate_n_1 : ARG0 x { IND + }.
            match = _predicate_entry_re.match(line)
            if match is not None:
                pred = match.group('pred')
                if pred not in data['predicates']:
                    data['predicates'][pred] = {
                        'supertypes': [],
                        'synopses': []
                    }
                sups = match.group('supertypes')
                if sups:
                    data['predicates'][pred]['supertypes'] = sups.split(' & ')
                synposis = match.group('synposis')
                roles = []
                if synposis:
                    for rolematch in _synopsis_re.finditer(synposis):
                        d = rolematch.groupdict()
                        rprops = d['properties'] or []
                        if rprops:
                            pairs = rprops.split(', ')
                            rprops = [tuple(pair.split()) for pair in pairs]
                        roles.append(
                            Role(d['role'], d['value'], rprops, bool(d['opt']))
                        )
                    data['predicates'][pred]['synopses'].append(tuple(roles))

    return data

class SemI(object):
    """
    A semantic interface.

    SEM-Is describe the semantic inventory for a grammar. These include
    the variable types, valid properties for variables, valid roles
    for predications, and a lexicon of predicates with associated roles.

    Args:
        variables: a mapping of (var, Variable)
        properties: a mapping of (prop, Property)
        roles: a mapping of (role, Role)
        predicates: a mapping of (pred, Predicate)
    """
    def __init__(self,
                 variables=None,
                 properties=None,
                 roles=None,
                 predicates=None):
        self.variables = dict(variables) if variables is not None else {}
        self.properties = dict(properties) if properties is not None else {}
        self.roles = dict(roles) if roles is not None else {}
        self.predicates = dict(predicates) if predicates is not None else {}

    @classmethod
    def from_dict(cls, d):
        """
        Instantiate a SemI from a dictionary representation.
        """
        read = lambda cls: (lambda pair: (pair[0], cls.from_dict(pair[1])))
        return cls(
            variables=map(read(Variable), d.get('variables', {}).items()),
            properties=map(read(Property), d.get('properties', {}).items()),
            roles=map(read(Role), d.get('roles', {}).items()),
            predicates=map(read(Predicate), d.get('predicates', {}).items())
        )

    def to_dict(self):
        """
        Return a dictionary representation of the SemI.
        """
        make = lambda pair: (pair[0], pair[1].to_dict())
        return dict(
            variables=dict(make(v) for v in self.variables.items()),
            properties=dict(make(p) for p in self.properties.items()),
            roles=dict(make(r) for r in self.roles.items()),
            predicates=dict(make(p) for p in self.predicates.items())
        )
