#
# Templet Class
#
# Accepts syntax descriptions that are reminiscent of usage strings and converts 
# them into docgen productions.
#

# Imports {{{1
from __future__ import print_function
from .production import Production
from .predefined import Alternates, Optional, Join, Cull, Tab, Newline
from .tabfiles import TabFiles
import ply.lex, ply.yacc
import collections
import os

# Globals {{{1
Tabstop = None
IRs = {}    # Cache for the intermediate representations
            # (avoids compiling the same templet over and over)

# Utilities {{{1
def toStr(arg):
    if type(arg) == list:
        return '[%s]' % ', '.join([toStr(a) for a in arg])
    else:
        return str(arg)

def flatten(iterable):
    for each in iterable:
        if isinstance(each, list):
            for e in flatten(each):
                yield e
        else:
            yield each


# Attributes class {{{1
# The user passes in attribute values to the templet using args and kwargs. The 
# Attributes class is responsible for searching through potential attribute 
# values and finding the one with the right name. The kwargs are searched 
# directly. Items in args may either be dictionaries or objects, and the first 
# item or attribute with the right name is used. This class also handles 
# hierarchical attributes; attributes of the form 'container.name' that may be 
# nested arbitrarily deeply.

class Attributes(object):
    def __init__(self, nullify, args, kwargs):
        self.nullify = nullify
        self.args = args
        self.kwargs = kwargs

    def getValue(self, key, indices, _collection = None):
        if '.' in key:
            topkey, subkey = key.split('.', 1)
            collection = self._get(topkey, _collection)
            if collection:
                if type(collection) == list:
                    val = self.getValue(
                        key=subkey,
                        indices=indices[1:],
                        _collection=indices[0].select(collection)
                    )
                else:
                    val = self.getValue(subkey, indices, collection)
            else:
                val = None
        else:
            if indices:
                val = self._get(key, _collection)
                for i in range(len(indices)):
                    val = indices[i].select(val)
            else:
                val = self._get(key, _collection)
        return val

    def _get(self, key, collection):
        # Get the value from a collection of values accommodating the fact that 
        # the collection may be a dictionary or an object, and accommodating 
        # both top level keys (search the collections given when this class was 
        # instantiated) or a lower level key (search the collection specified 
        # by the higher level keys).

        if collection is None:
            # this is a top-level attribute
            collections = [self.kwargs] + list(self.args)
        else:
            # this is a lower-level attribute of the given collection
            collections = [collection]
        for arg in collections:
            try:
                return getattr(arg, key)
            except (AttributeError, TypeError):
                pass
            try:
                return arg[key]
            except (KeyError, AttributeError, TypeError):
                pass
        if self.nullify:
            return None
        raise TempletError('%s: not found.' % key)

# Index class {{{1
class Index(object):
    # Class that handles accessing of array values for joins. One object of 
    # this class is intended to handle one dimension of perhaps 
    # a multi-dimensional access. These objects are always expected to be 
    # passed around in an array so as to allow support of multi-dimensional 
    # indexing.
    def __init__(self, initialValue=0):
        self.index = initialValue
        self.arrayFound = False

    def increment(self):
        if not self.arrayFound:
            raise IndexError
        self.index += 1

    def select(self, data):
        if type(data) is not str and isinstance(data, collections.Iterable):
            self.arrayFound = True
            return data[self.index]
        else:
            return data

    def __repr__(self):
        return 'Index(%s)' % (self.index)

# Deferred classes {{{1
# Deferred classes are used as the intermediate representation (IR) for the 
# parser, meaning that that input text is broken down into its recognizable 
# parts and converted into a hierarchical collection of strings, lists, and 
# objects of the Deferred class.  This is done so as to defer the conversion 
# into docgen productions until the join processing can occur. In join 
# processing, subelements are evaluated repeatedly while iterating over the 
# items of a list.

# Deferred class {{{2
class Deferred(object):
    def __init__(self, arg=None):
        self.arg = arg

    def expand(self, attrs, index=()):
        rule = []
        for arg in flatten([self.arg]):
            if isinstance(arg, Deferred):
                rule.append(arg.expand(attrs, index))
            else:
                assert isinstance(arg, (type(None), str, Production)) , ('found %s' % type(arg))
                rule.append(arg)
        return self._finalize(rule)

    def _finalize(self, rule):
        return Production(rule)

    def __str__(self):
        return '%s' % toStr(self.arg)


# Deferred Attribute class {{{2
class DeferredAttribute(Deferred):
    def expand(self, attrs, index=()):
        return attrs.getValue(self.arg, index)

    def _finalize(self, rules):
        return rules

    def __str__(self):
        return '<%s>' % self.arg


# Deferred Alternates class {{{2
class DeferredAlternates(Deferred):
    def _finalize(self, rules):
        return Alternates(rules)

    def __str__(self):
        return '{%s}' % '|'.join([toStr(arg) for arg in self.arg])


# Deferred Optional class {{{2
class DeferredOptional(Deferred):
    def _finalize(self, rules):
        assert len(rules) == 1
        rule = rules[0]
        return Optional(rule, alt="")

    def __str__(self):
        return '{[%s]}' % toStr(self.arg)


# Deferred Join class {{{2
class DeferredJoin(Deferred):
    def __init__(self, arg, sep):
        self.arg = arg
        self.sep = sep

    def expand(self, attrs, indices=()):
        arg = self.arg
        rules = []
        innerIndex = Index()
        indices = indices + (innerIndex,)
        try:
            while(True):
                if isinstance(arg, Deferred):
                    rules.append(arg.expand(attrs, indices))
                else:
                    assert type(arg) in [type(None), str], ('found %s' % type(arg))
                    rules.append(arg)
                innerIndex.increment()
        except IndexError:
            pass
        return self._finalize(rules)

    def _finalize(self, rules):
        return Join(rules, self.sep)

    def __str__(self):
        return '{%s}%s...' % (toStr(self.arg), self.sep)


# Deferred Tab class {{{2
class DeferredTab(Deferred):
    def _finalize(self, rules):
        return Tab(Tabstop)

    def __str__(self):
        return 'Tab(%s)' % Tabstop


# Exceptions {{{1
class TempletError(Exception):
    def __init__(self, message, lexer=None, lexpos=None):
        self.message = message
        if lexer:
            bol = lexer.lexdata.rfind('\n', 0, lexpos) + 1
            if bol < 0:
                bol = 0
            eol = lexer.lexdata.find('\n', lexpos)
            if eol < 0:
                eol = len(lexer.lexdata)
            self.line = lexer.lexdata[bol:eol]
            self.col = (lexpos - bol)
            self.row = lexer.lexdata[0:lexpos].count('\n') + 1

    def __str__(self):
        "Return a formatted error message."
        try:
            location = "%s\n%s^\n%s: " % (
                self.line, self.col*' ', self.row
            )
        except AttributeError:
            location = ""
        return location + self.message

# Lexer {{{1
reserved = {}
tokens = [
    'ID',
    'LBRACE',
    'RBRACE',
    'LANGLE',
    'RANGLE',
    'LBRACKET',
    'RBRACKET',
    'PIPE',
    'DOTS',
    'TAB',
    'NEWLINE',
    'CHAR',
]
literals = ''

# Regular expressions that define identifiers
def t_ID(t):
    r'\b\w+(\.\w+)*\b'
    t.type = reserved.get(t.value, 'ID')    # check for reserved words
    return t

# Regular expressions that define the symbols
# Could not put these in literals because I wanted to allow excaping
t_LBRACE = r"(?<!\\){"      # matches { if not preceded by \
t_RBRACE = r"(?<!\\)}"      # matches } if not preceded by \
t_LANGLE = r"(?<!\\)<"      # matches < if not preceded by \
t_RANGLE = r"(?<!\\)>"      # matches > if not preceded by \
t_LBRACKET = r"(?<!\\)\["   # matches [ if not preceded by \
t_RBRACKET = r"(?<!\\)\]"   # matches ] if not preceded by \
t_PIPE = r"(?<!\\)\|"       # matches | if not preceded by \
t_DOTS = r"\.\.\."          # matches ...
t_TAB = r"\t"               # matches tab
t_NEWLINE = r"\n"           # matches newline
t_CHAR = r"."               # matches any character not previously matched
                            # ply orders tokens from longest to shortest, so 
                            # this one must be the shortest

# Error handler (this should never happen)
def t_error(t):
    c = t.value[0]
    lexer, lexpos = t.lexer, t.lexpos
    t.lexer.skip(1)
    raise TempletError("illegal character '%s'." % c, lexer, lexpos)

# Parser {{{1
def p_template(p):
    "template : alternatives"
    if len(p[1]) > 1:
        p[0] = DeferredAlternates(p[1])
    else:
        p[0] = Deferred(p[1])

def p_simple_template(p):
    "template : text"
    p[0] = Deferred(p[1])

def p_alternatives(p):
    "alternatives : alternatives PIPE terms"
    p[0] = p[1] + [Deferred(p[3])]

def p_alternatives_last1(p):
    "alternatives : terms"
    p[0] = [Deferred(p[1])]

def p_alternatives_last2(p):
    "alternatives : text terms"
    p[0] = [Deferred(['%s' % p[1]] + p[2])]

def p_terms(p):
    "terms : terms term"
    p[0] = p[1] + p[2]

def p_terms_last(p):
    "terms : term"
    p[0] = p[1]

def p_term(p):
    """
    term : atom text
    """
    p[0] = [p[1], '%s' % p[2]]

def p_naked_term(p):
    """
    term : atom
    """
    p[0] = [p[1]]

def p_atom(p):
    """
    atom : attribute
         | group
         | option
         | join
         | tab
         | newline
    """
    p[0] = Deferred(p[1])

def p_attribute(p):
    "attribute : LANGLE ID RANGLE"
    p[0] = DeferredAttribute(p[2])

def p_group(p):
    "group : LBRACE template RBRACE"
    p[0] = p[2]

def p_option(p):
    "option : LBRACKET template RBRACKET"
    p[0] = DeferredOptional(p[2])

def p_join(p):
    "join : atom text DOTS"
    p[0] = DeferredJoin(p[1], p[2])

def p_tab(p):
    "tab : TAB"
    p[0] = DeferredTab()

def p_newline(p):
    "newline : NEWLINE"
    p[0] = Newline('!')

def p_text(p):
    """
    text : text CHAR
         | text ID
    """
    p[0] = p[1] + (p[2] if p[2] != '\\' else '')

def p_text_last(p):
    """
    text : CHAR
         | ID
    """
    p[0] = p[1] if p[1] != '\\' else ''

# Error rule for syntax errors
def p_error(p):
    if p:
        raise TempletError("syntax error at '%s'." % (p.value), p.lexer, p.lexpos)
    else:
        raise TempletError("premature end of content.")

# Parse {{{1
tabfiles = TabFiles(__file__)
Lexer = ply.lex.lex(
    debug=False
  , optimize=True
  , lextab=tabfiles.lextab
  , outputdir=tabfiles.directory
)
Parser = ply.yacc.yacc(
    debug=False
  , optimize=True
  , method="SLR"
  , tabmodule=tabfiles.parsetab
  , outputdir=tabfiles.directory
)
def parse(templet, attrs):
    if templet not in IRs:
        IRs[templet] = Parser.parse(templet, lexer=Lexer)
    return IRs[templet].expand(attrs)

# Templet class {{{1
class Templet(Production):
    def __init__(self, templet, *args, **kwargs):
        self.add_rule(self._elaborate(self, templet, *args, **kwargs))

    def add_templet(self, templet, *args, **kwargs):
        self.rule = self._clean(self._elaborate(self, templet, *args, **kwargs))

    @staticmethod
    def _elaborate(rule, templet, *args, **kwargs):
        global Tabstop
        Tabstop = kwargs.pop('tabstop', None)
        nullify = kwargs.pop('nullify', None)
        attrs = Attributes(nullify, args + (rule,), kwargs)
        if type(templet) == str:
            return parse(templet, attrs)
        elif isinstance(templet, collections.Iterable):
            for each in templet:
                result = parse(each, attrs)
                if result:
                    return result
        raise AssertionError("Invalid type for templet value")
