# Predefined Docgen Productions
#
# These productions are either generally useful or require special magic (such 
# as indent codes) that we really don't want users to be using.

# Imports {{{1
from __future__ import print_function, division
from .preferences import DEFAULT_TABSTOP
from .production import (
    Production, newlineCode, tabCode, killCode, indentCode, leaderCode, 
    dedentCode, emptyCode
)
import collections
import re


# Alternates {{{1
class Alternates(Production):
    """Choose between alternate rules.  Use the first that works.

    :param rules: A list of rules.

    >>> from docgen import Alternates

    >>> choice = Alternates([None, 'coffee', 'tea'])
    >>> print str(choice)
    coffee

    """
    def __init__(self, rules):

        # expand all rules and identify the null ones
        # but do not declare a list null if it contains an null member
        rules = self._clean(rules, cullArrays=False)

        if rules is None:
            self.rule = None
            return
        for rule in rules:
            if rule is not None:
                self.rule = rule
                return
        self.rule = None


# Block {{{1
class Block(Production):
    """
    A list where the contents of the list are indented and each item is placed 
    on a separate line.

    :param items:
        A list of rules. Each will be indented and set on its own line.
    :param sep:
        A rule that is added to the end of each item except the last. The actual 
        separator used will be *sep* followed by Newline().
    :param lcap:
        A rule that is precedes the first item. It is not indented.
    :param rcap:
        A rule that is follows the last item. It is not indented.
    :param term:
        A rule that is added to the end of the last item.
    :param ifempty:
        What to do if the argument is None or an empty list. Choices are
            'error': raise a ValueError,
            'discard': return None to contaminate the containing rule,
            'caps': return the end caps surrounding a newline,
            'capsonly': return the end caps surrounding an empty string.
        Anything else simply replaces the empty list.
    :param ifsingle:
        What to do if the argument is a list with only one item.  Choices are
            'caps': print the newline, indent, and end caps,
            'nocaps': print newline and indent, but not the end caps.
            'bare': do not print newline, indent or end caps.

    >>> from docgen import Block, Join

    >>> stmt = Join([
    ...     'if (doit)',
    ...     Block(['t = a;', 'a = b;', 'b = t;'], lcap='begin', rcap='end')
    ... ])
    >>> print str(stmt)
    if (doit) begin
        t = a;
        a = b;
        b = t;
    end

    >>> print(Block([], ifempty='error'))
    Traceback (most recent call last):
    ...
    ValueError: Block must not be empty.

    >>> print(Block([], ifempty='discard'))
    <BLANKLINE>

    >>> print(Block([], ifempty='caps'))
    (
    )

    >>> print(Block([], ifempty='capsonly'))
    ()

    >>> print(Block(['a'], ifsingle='caps'))
    (
        a
    )

    >>> print(Block([], ifempty='...'))
    (
        ...
    )

    >>> print(Block(['a'], ifsingle='nocaps'))
    <BLANKLINE>
        a

    >>> print(Block(['a'], ifsingle='bare'))
    a

    >>> print(Block([], ifempty='<empty>', ifsingle='bare'))
    <empty>

    """
    def __init__(
        self,
        items,
        sep='', lcap='(', rcap=')', term=emptyCode(),
        ifempty='discard',
        ifsingle='caps'
    ):
        if ifempty != 'caps':
            items = DiscardIfEmpty(items)
        if not items:
            if ifempty == 'discard':
                self.add_rule(None)
                return
            elif ifempty == 'error':
                raise ValueError('Block must not be empty.')
            elif ifempty not in ['caps', 'capsonly']:
                items = [ifempty]

        if items and len(items) == 1:
            if ifsingle == 'nocaps':
                lcap = rcap = ''
            elif ifsingle == 'bare':
                self.add_rule(Join(items))
                return

        self.add_rule([
            lcap,
            Optional(
                Indent(
                    Join(
                        items,
                        sep=[sep, Newline()], rcap=term
                    )
                )
            ),
            rcap
        ])

# Cull {{{1
class Cull(Production):
    """Remove any null rules from a list.

    :param rules:
        A list of rules. Each will be examined to determine if it is inactive 
        and if so, it will be removed from the list.
    :param nonempty:
        A boolean. If true, the resulting list is expected to be nonempty. If it 
        is not, the entire production is nulled. DEPRECATED, use 
        DiscardIfEmpty() instead.

    >>> from docgen import Cull, Join

    >>> choices = Join(Cull([None, 'coffee', 'tea']))
    >>> print str(choices)
    coffee tea

    >>> from docgen import Optional, Join, Indent, Lines

    >>> analog = Optional([
    ...     Join(['analog', 'begin']),
    ...     Indent(Lines(Cull(['a = 1;', None, 'c = 3;']))),
    ...     'end'
    ... ])
    >>> print str(analog)
    analog begin
        a = 1;
        c = 3;
    end

    If the argument *nonempty* is true, then None is returned if the resulting 
    list is empty after all the null rules have been culled.

    >>> analog = Optional(Join([
    ...     'analog',
    ...     'begin',
    ...     Indent(Cull([None, None, None], nonempty=True)),
    ...     'end'
    ... ]))
    >>> print str(analog)
    <BLANKLINE>

    """
    def __init__(self, rules, nonempty=True):

        # expand all rules and identify the null ones
        # but do not declare a list null if it contains an null member
        rules = self._clean(rules, cullArrays=False)

        if rules is None:
            self.rule = None
            return
        self.rule = [rule for rule in rules if rule is not None]
        if nonempty and len(self.rule) == 0:
            self.rule = None

    def __len__(self):
        return len(self.rule)

    def __iter__(self):
        return iter(self.rule)


# DiscardIfEmpty {{{1
class DiscardIfEmpty(Production):
    """Convert rule to null if it is an empty list.

    :param rules:
        A list of rules.

    >>> from docgen import DiscardIfEmpty, Join

    >>> choices = Join(DiscardIfEmpty(['coffee', 'tea']))
    >>> nochoices = Join(DiscardIfEmpty([]))
    >>> print str(choices)
    coffee tea
    >>> print str(nochoices)
    <BLANKLINE>

    """
    def __init__(self, rules):
        self.rule = rules if rules else None

    def __len__(self):
        return len(self.rule)

    def __iter__(self):
        return iter(self.rule)


# Indent {{{1
class Indent(Production):
    """Increase the indent level.

    The contents are indented by the specified number of levels. If not 
    specified, the default is indenting one level. Nested indents accumulate.
    By default, a level of indent is 4 spaces, but this can be overridden when 
    using the expand() Production method.

    There are two indenting productions, this Indent and Indented().

    Indent() uses immediate indenting and dedenting. As such, it requires 
    a newline before the indenting and before the dedenting. The leading newline 
    is often provided by the surrounding rules and the trailing newline is often 
    provided by the contents, though can be provided by the Indent production 
    itself. By default Indent provides both leading and trailing newlines even 
    though they are often redundant, because if they are redundant the extra 
    newlines are automatically suppressed. Generally Indent is used with 
    conventional productions.

    :param contents:
        The rule to be indented.
    :param levels:
        An integer that specifies the number of levels to indent.
    :param newlines:
        Specify 'b' (short for both) if newlines should be added before an after 
        contents (default).  Specify 'l' (short for leading) if newline should 
        be added before the contents.  Specify 't' (short for trailing) if 
        newline should be added after the contents.  Specify 'n' (short for 
        none) if newlines should not be added to the contents.

    >>> from docgen import Indent, Production

    >>> whattodo = Production(['if today == doomsday:', Indent('panic')])
    >>> print str(whattodo)
    if today == doomsday:
        panic

    """
    def __init__(self, contents, levels=1, newlines='b'):
        lcap = Newline() if newlines in ['l', 'b'] else emptyCode()
        rcap = Newline() if newlines in ['t', 'b'] else emptyCode()
        self.add_rule([
            lcap,
            indentCode(levels),
            contents,
            rcap,
            dedentCode(levels),
        ])


# Indented {{{1
class Indented(Production):
    """Increase the indent level.

    There are two indenting productions, Indent() and Indented().

    Indented() uses deferred indenting and dedenting. As such, it expects 
    indenting to be provided on the first line and a newline to immediately 
    follow the production. This is generally true in templates. Generally 
    Indented() is used with templates and Indent() is used with individual 
    productions.

    :param contents:
        The rule to be indented.
    :param levels:
        An integer that specifies the number of levels to indent.
    :param newlines:
        Specify 'b' (short for both) if newlines should be added before an after 
        contents (default).  Specify 'l' (short for leading) if newline should 
        be added before the contents.  Specify 't' (short for trailing) if 
        newline should be added after the contents.  Specify 'n' (short for 
        none) if newlines should not be added to the contents.

    >>> from docgen import Indented, Template, Lines, Newline
    >>> from textwrap import dedent

    >>> whattodo = Template(dedent('''\
    ...     if today == doomsday:
    ...         {Indented(Lines(['panic', 'panic some more']))}
    ... '''), symbols=globals())
    >>> print str(whattodo)
    if today == doomsday:
        panic
        panic some more

    """

    def __init__(self, contents, newlines='n', levels=1):
        lcap = Newline() if newlines in ['l', 'b'] else emptyCode()
        rcap = Newline() if newlines in ['t', 'b'] else emptyCode()
        self.add_rule([
            lcap,
            indentCode(levels, deferred=True),
            contents,
            rcap,
            dedentCode(levels, deferred=True),
        ])


# Join {{{1
class Join(Production):
    """Put separators between the members of a list and add end caps.

    :param rules:
        A list of rules. Each will be joined together using the separator to 
        form a single unit.
    :param sep:
        A rule that is placed between each rule in rules.
    :param lcap:
        A rule that is placed before the first rule in rules.
    :param rcap:
        A rule that is placed after the last rule in rules.

    >>> from docgen import Join

    >>> l = Join(['a', 'b', 'c'])
    >>> print str(l)
    a b c

    >>> l = Join(['a', 'b', 'c'], sep=', ', lcap='<', rcap='>')
    >>> print str(l)
    <a, b, c>

    """
    def __init__(self, rules, sep=' ', lcap=emptyCode(), rcap=emptyCode()):

        # expand all rules and identify the null ones
        rules = self._clean(rules)

        if rules is None:
            self.rule = None
            return

        sep = self._clean(sep)
        lcap = self._clean(lcap)
        rcap = self._clean(rcap)

        if rules:
            # perform the join by creating array twice as long as original 
            # filled with the separator, then do an interleaved assignment to 
            # write over every other separator with a rule in the given list.
            self.rule = [lcap] + (2*len(rules)-1)*[sep] + [rcap]
            self.rule[1::2] = rules
        else:
            self.rule = [lcap, rcap]


# Leader {{{1
class Leader(Production):
    """Add a leader to each line.

    :param contents:
        The rule that, when expanded, will have the leader appended to the 
        beginning of each line.
    :param leader:
        The key (a string) for the desired leader. The available leaders are 
        specified in a dictionary to the expand() method when the production is 
        expanded. If a dictionary of leaders will not be provided when 
        expanding, you must specify a key for one of the default leaders, which 
        includes '*' (maps to ' * '), '#' (maps to '# '), and '//' (maps to '// 
        ').

    >>> from docgen import Leader, Production, Newline, Lines, Indented

    >>> class Comment(Production):
    ...     def __init__(self, lines):
    ...         self.add_rule([
    ...             '/*', Newline(),
    ...             Leader(Lines(lines), '*'),
    ...             Newline(), ' */'
    ...         ])

    >>> title = "Leader"
    >>> description = dedent('''\
    ...     Leader adds a fixed string to the beginning of every line.
    ...     If differs in a fundamental way from Join in that it acts on lines, 
    ...     whereas Join acts on items in a list.
    ... ''')
    >>> comment = Comment([title, '', description])
    >>> print str(comment)
    /*
     * Leader
     *
     * Leader adds a fixed string to the beginning of every line.
     * If differs in a fundamental way from Join in that it acts on lines,
     * whereas Join acts on items in a list.
     */

    It is possible to nest both indents and leaders.

    """
    def __init__(self, contents, leader):
        self.add_rule([
            leaderCode(leader), contents, dedentCode(1), Newline()
        ])


# Lines {{{1
class Lines(Production):
    """Put each entry of a list onto its own line.

    :param items:
        A list of rules. Each will be placed on its own line as it is joined 
        together to form a single unit.

    >>> from docgen import Lines

    >>> l = Lines(['coffee', 'tea', '', 'me'])
    >>> print str(l)
    coffee
    tea
    <BLANKLINE>
    me

    """

    def __init__(self, items):
        self.add_rule(Join(items, sep=Newline(), rcap=Newline()))


# Newline {{{1
class Newline(Production):
    """Adds one or more newlines to the output.

    :param blankLines:
        The number of newlines output equals the number needed to produce number 
        of blank lines specified on the constructor.  Any adjacent newline 
        commands are combined with the result being the number of blank lines 
        produced is the largest specified on any of the adjacent commands.

        If instead '!' is passed to the constructor, Newline acts like a
        conventional newline (adjacent newlines are not combined).

    >>> from docgen import Newline, Production

    >>> l = Production(
    ...     ['coffee', Newline(), Newline(), 'tea', Newline(1), 'me']
    ... )
    >>> print str(l)
    coffee
    tea
    <BLANKLINE>
    me

    >>> l = Production(
    ...     ['coffee', Newline('!'), Newline('!'), 'tea', Newline(2), 'me']
    ... )
    >>> print str(l)
    coffee
    <BLANKLINE>
    tea
    <BLANKLINE>
    <BLANKLINE>
    me

    """
    def __init__(self, blankLines=0):
        self.add_rule(newlineCode(blankLines))


# Op {{{1
class Op(Production):
    """An operator with operands.

    Takes a single operator and a list of operands. The list is joined by the 
    operator.

    :param operator:
        A rule that becomes the operator.
    :param operands:
        A list of rules that become the operands. The list is culled of null 
        rules, and if the resulting list is empty, the operator itself becomes 
        null.
    :param group:
        If false, do not delimit the expression. If true, delimit with 
        parentheses. Otherwise a string of two characters or a string with two 
        groups of characters separated by white space should be specified, and 
        the expression will be delimited by those.  group is ignored if there 
        are less than two operands.
    :param multiline:
        A boolean. If true, place each delimiter and operand on a separate line.
    :param squeeze:
        A boolean. If true, do not put spaces around the operator.

    >>> from docgen import Op

    >>> expr = Op('+', ['a', 'b', 'c'])
    >>> print str(expr)
    a + b + c

    >>> expr = Op(':', ['a', 'b', 'c'], group='[]', squeeze=True)
    >>> print str(expr)
    [a:b:c]

    >>> assign = Op('=', [
    ...     'x',
    ...     Op('+', ['a', 'b', 'c'], group=True, multiline=True)
    ... ])
    >>> print str(assign)
    x = (
        a +
        b +
        c
    )

    >>> expr = Op('+', ['a', 'b', None, 'd'])
    >>> print str(expr)
    <BLANKLINE>

    """
    def __init__(
        self, operator, operands, group=None, multiline=False, squeeze=False
    ):
        if group and operands and len(operands) > 1:
            if group is True:
                group = '()'
            if len(group) != 2:
                group = group.split
            assert len(group)
            lcap = group[0]
            rcap = group[1]
        else:
            lcap = emptyCode()
            rcap = emptyCode()
        sep = operator if squeeze else [' ', operator, ' ']
        joiner = Block if multiline else Join

        self.add_rule(joiner(
            operands, sep=sep, lcap=lcap, rcap=rcap
        ))

# Optional {{{1
class Optional(Production):
    """An optional rule.

    The rule is removed and replaced by alt if it resolves to None.

    :param rule:
        The rule that this production evaluates to as long as this rule is 
        active.
    :param alt:
        The rule that this production evaluates to if the above rule is 
        inactive.

    >>> from docgen import Optional

    >>> result = Optional('Mr. Bill', alt='Oh no!')
    >>> print str(result)
    Mr. Bill

    >>> result = Optional(None, alt='Oh no!')
    >>> print str(result)
    Oh no!

    """
    def __init__(self, rule, alt=emptyCode()):
        self.add_rule(rule)
        if not self:
            self.add_rule(alt)


# ParenList {{{1
class ParenList(Production):
    """
    A list with parentheses as end caps and commas as the separator.

    :param items:
        A list of rules. Each will be joined together using the separator to 
        form a single unit.
    :param sep:
        A rule that is placed between each rule in rules.
    :param lcap:
        A rule that is placed before the first rule in rules.
    :param rcap:
        A rule that is placed after the last rule in rules.

    >>> from docgen import ParenList

    >>> args = ParenList(['a', 'b', 'c'])
    >>> print str(args)
    (a, b, c)

    """
    def __init__(self, items, sep=', ', lcap='(', rcap=')'):
        self.add_rule(Join(items, sep=sep, lcap=lcap, rcap=rcap))

# Required {{{1
class Required(Production):
    """A required rule.

    The rule is passed unchanged after assuring that it is not null. If it is 
    null, an exception is thrown.

    :param rule:
        The rule that this production evaluates to as long as this rule is 
        active. If this rule is not active, this production throws a ValueError 
        exception.

    >>> from docgen import Required

    >>> result = Required('Mr. Bill')
    >>> print str(result)
    Mr. Bill

    >>> result = Required(None)
    Traceback (most recent call last):
    ...
    ValueError: required production is null.

    """
    def __init__(self, rule):
        self.add_rule(rule)
        if not self:
            raise ValueError('required production is null.')


# StrikeLine {{{1
class StrikeLine(Production):
    """The current line of text is deleted.

    >>> from docgen import StrikeLine, Newline, Optional, Leader, Lines

    >>> class Comment(Production):
    ...     def __init__(self, lines):
    ...         self.add_rule([
    ...             '/*', Newline(),
    ...             Leader(Lines(lines), '*'),
    ...             Newline(), ' */'
    ...         ])

    >>> title = 'Monte Python'
    >>> description = 'A British surreal comedy group.'
    >>> comment = Comment([title, Optional(description, StrikeLine())])
    >>> print str(comment)
    /*
     * Monte Python
     * A British surreal comedy group.
     */

    >>> description = None
    >>> comment = Comment([title, Optional(description, StrikeLine())])
    >>> print str(comment)
    /*
     * Monte Python
     */

    """
    def __init__(self):
        self.add_rule(killCode())


# Tab {{{1
class Tab(Production):
    """Adds a horizontal tab to specified column.

    If current column is already beyond specified column, a newline is output 
    before the tab.

    :param column:
        The desired column of the tab stop.

    >>> from docgen import Tab, Production

    >>> result = Production(['Name:', Tab(8), 'Carlos Danger'])
    >>> print str(result)
    Name:   Carlos Danger

    >>> result = Production(['Name:', Tab(4), 'Carlos Danger'])
    >>> print str(result)
    Name:
        Carlos Danger

    """
    def __init__(self, column=None):
        if column is None:
            column = DEFAULT_TABSTOP
        self.add_rule(tabCode(column))
