# Peoduction class
#
# The foundation of docgen. Everything is based on this class.

# Imports {{{1
from __future__ import print_function, division
from .preferences import (
    DEFAULT_INDENT, DEFAULT_LEADERS, STRIP_TRAILING_WHITESPACE
)
import collections
import re
from six import string_types


# Globals {{{1
# do not import these codes, if you need them, use with control code functions
CTRL_CODE = '\a'
NEWLINE_CODE = CTRL_CODE + 'n'
TAB_CODE = CTRL_CODE + 't'
KILL_CODE = CTRL_CODE + 'k'
INDENT_CODE = CTRL_CODE + 'i'
LEADER_CODE = CTRL_CODE + 'l'
DEDENT_CODE = CTRL_CODE + 'd'
DEFERRED_INDENT_CODE = CTRL_CODE + 'I'
DEFERRED_LEADER_CODE = CTRL_CODE + 'L'
DEFERRED_DEDENT_CODE = CTRL_CODE + 'D'
EMPTY_CODE = CTRL_CODE + 'e'


# Control Codes {{{1
def newlineCode(blankLines=0):
    """Insert newlines

    Adjacent newline codes are combined with the resulting number of blank lines 
    being the maximum specified on any of the adjacent codes. By default, the 
    number of blank lines is 0.  Use blankLines='!' to defeat the combining of 
    newlines.

    :param blankLines: The desired number of blank lines.
    """
    return NEWLINE_CODE + str(blankLines)

def tabCode(column):
    """Inserts space until the specified column is reached

    :param column: The column of the desired tabstop.
    """
    return TAB_CODE + str(column)

def killCode():
    """Strikes the current line"""
    return KILL_CODE

def indentCode(levels=1, deferred=False):
    """Start adding indent to each line

    :param levels:
        The desired number of indent levels.
    :param deferred:
        The indent takes effect on the next line if deferred is True and the 
        current line otherwise.
    """
    if deferred:
        return DEFERRED_INDENT_CODE + str(levels)
    else:
        return INDENT_CODE + str(levels)

def leaderCode(key, deferred=False):
    """Start adding leader to each line

    :param key:
        The key for the desired leader must be specified.
    :param deferred:
        The indent takes effect on the next line if deferred is True and the 
        current line otherwise.
    """
    if deferred:
        return DEFERRED_LEADER_CODE + str(key)
    else:
        return LEADER_CODE + str(key)

def dedentCode(levels=1, deferred=False):
    """Discard one or more levels of indents or leaders

    :param levels:
        The desired number of indents or leaders to discard.
    :param deferred:
        The indent takes effect on the next line if deferred is True and the 
        current line otherwise.
    """
    if deferred:
        return DEFERRED_DEDENT_CODE + str(levels)
    else:
        return DEDENT_CODE + str(levels)

def emptyCode():
    """No-op.

    Use this when you need to specify a rule but want DocGen to act like you did 
    not. The distinction between this and an empty string is significant when 
    processing Newlines(). Two Newlines() separated by an emptyCode() will be 
    combined, where as they will not if they are separated by an empty string.
    """
    return EMPTY_CODE

# Utilities {{{1
# strip_trailing_whitespace() {{{2
if STRIP_TRAILING_WHITESPACE:
    def strip_trailing_whitespace(text):
        return '\n'.join([line.rstrip(' \t') for line in text.splitlines()])

        # the above strips any trailing newline, the following does not
        #return (
        #    '\n'.join([line.rstrip(' \t') for line in text.splitlines()]) +
        #    '\n' if text[-1] == '\n' else ''
        #)
else:
    def strip_trailing_whitespace(text):
        return text

# is_str() {{{2
def is_str(obj):
    return isinstance(obj, string_types)

# is_iterable {{{2
def is_iterable(obj):
    return isinstance(obj, collections.Iterable)


# Production class {{{1
class Production(object):
    # Description {{{2
    """A Description of a Syntax Fragment

    A Production takes a rule as input, and it converts that rule to a string 
    when the render() method is called or when a production object is cast to 
    a string using the str() operator.

    :param rule:
        A rule may be either a string, a Production object, or a list of 
        rules.  In this way a rule may be a hierarchical object of arbitrary 
        depth.  If a rule is None, or contains a rule that is None, then the 
        production is not active, meaning that it will not generate any 
        output.
    """

    # Constructor {{{2
    def __init__(self, rule):
        #assert Production._eligibleToBeRule(rule), ('Bad type: %s' % type(rule))
        self.add_rule(rule)

    # add_rule() {{{2
    def add_rule(self, rule):
        """Add rule

        Used by subclasses to add the rule to the production.
        """
        self.rule = self._clean(rule)

    # _clean() {{{2
    @staticmethod
    def _clean(self, cullArrays = True):
        # Returns its argument if the argument does not evaluate to None. It
        # evaluates to None if it is None, or if it is iterable and one of its
        # members is None.
        # Is a static method because self may not be instance of Production.
        if hasattr(self, 'docgen_rule'):
            assert not hasattr(self, 'docgen_template')
            assert not hasattr(self, 'docgen_templet')
            return Production._clean(self.docgen_rule())
        if hasattr(self, 'docgen_templet'):
            from .templet import Templet
            if isinstance(self.docgen_templet, collections.Callable):
                self = Templet._elaborate(self, self.docgen_templet())
            else:
                self = Templet._elaborate(self, self.docgen_templet)
        if hasattr(self, 'docgen_template'):
            from .template import Template
            symbols = getattr(self, 'docgen_symbols', None)
            if isinstance(self.docgen_template, collections.Callable):
                self = Template._elaborate(self, self.docgen_template(), symbols=symbols)
            else:
                self = Template._elaborate(self, self.docgen_template, symbols=symbols)
        if is_str(self):
            return self
        if isinstance(self, Production):
            return self if self.rule is not None else None
        if is_iterable(self):
            self = [Production._clean(each) for each in self]
            if cullArrays:
                return self if None not in self else None
            else:
                return self
        assert self is None, '%s has unexpected type: %s' % (self, type(self))
        return None

    # _flatten() {{{2
    @staticmethod
    def _flatten(self):
        # Flatten rule to produce a stream of tokens, where tokens are either 
        # control codes, strings, or Nones.
        # Is a static method because self may not be instance of Production.
        if is_str(self):
            # Remove any newlines off the ends of strings, they should be
            # specified explicitly with Newline().
            # Then split the string at any remaining newlines and yield each
            # segment individually while yielding a newline code between each
            # segment.
            segments  = self.strip('\n').split('\n')
            for each in segments[:-1]:
                assert CTRL_CODE not in each
                yield each
                yield NEWLINE_CODE + '!'
            yield segments[-1]
        elif is_iterable(self):
            for each in self:
                for sub in Production._flatten(each):
                    yield sub
        elif isinstance(self, Production):
            for each in Production._flatten(self.rule):
                yield each
        elif hasattr(self, 'docgen_rule') or hasattr(self, 'docgen_template'):
            for each in Production._flatten(Production._clean(self)):
                yield each
        elif self is not None:
            raise AssertionError("Bad type %s" % type(self))

    # _strikeLines() {{{2
    def _strikeLines(self):
        # partition a stream of tokens into groups that represent lines, and 
        # then kill those groups that contain line-kill codes. The first output 
        # line is treated specially, because output is assumed to start on 
        # a newline. Normally, a request for a blank line would result in two 
        # newlines being output, one to terminate the prior line, and one to 
        # create the blank line. But the first line is special because we do not 
        # need to terminate the prior line. Normally, we expect the user to know 
        # this and act accordingly, but things break down when the first lines 
        # are killed. To assure that we do not output extra newlines if lines 
        # are killed before there is any output, the newlineNeeded flag is 
        # initially set to False, and then set true once something has been 
        # output.
        line = []
        kill = False
        firstLine = True
        for token in self._flatten(self.rule):
            if token and token[0] == CTRL_CODE:
                code = token[0:2]
                if code == NEWLINE_CODE:
                    arg = token[2:]
                    if kill:
                        line = []
                        try:
                            newlines = int(arg)
                            if newlines >= 1:
                                if firstLine:
                                    newlines -= 1
                                yield NEWLINE_CODE + str(newlines)
                        except ValueError:
                            # this is a traditional newline (code=!), kill it too
                            pass
                    else:
                        for each in line:
                            if each and each[0] != CTRL_CODE:
                                firstLine = False
                            yield each
                        yield token
                        line = []
                    kill = False
                elif code == KILL_CODE:
                    kill = True
                elif code in [
                    INDENT_CODE, LEADER_CODE, DEDENT_CODE, DEFERRED_INDENT_CODE,
                    DEFERRED_LEADER_CODE, DEFERRED_DEDENT_CODE,
                ]:
                    # Pass these code through regardless of whether then line is 
                    # being killed, and do it now, their order is not important.
                    yield token
                else:
                    line.append(token)
            else:
                line.append(token)
        for each in line:
            yield each

    # _filter() {{{2
    def _filter(self, unitIndent, leaders):
        """
        Convert production to a string while filtering out suppressed sequences
        and extra newlines, and adding indenting.
        """
        column = 0
        indents = []
        deferredIndents = []
        deferredDedents = 0
        newlines = 0
        prev_newline_was_soft = False
        line = ''

        for item in self._strikeLines():
            if item and item[0] == CTRL_CODE:
                code = item[0:2]
                if code == NEWLINE_CODE:
                    arg = item[2:]
                    if not newlines:
                        yield ''.join(indents) + line
                        line = ''
                        column = sum([len(indent) for indent in indents])

                    if arg == '!': # this is a hard newline
                        if prev_newline_was_soft:
                            # if previous newline was soft, convert it to hard 
                            # but do not increment it.
                            prev_newline_was_soft = False
                        else:
                            newlines = newlines + 1
                    else:          # this is a soft newline
                        newlines = max(newlines, int(arg) + 1)
                        prev_newline_was_soft = True

                    # Process any deferred indents and dedents
                    if deferredIndents:
                        indents += deferredIndents
                        deferredIndents = []
                    if deferredDedents:
                        assert len(indents) >= deferredDedents, (
                            'too many deferred dedents'
                        )
                        indents = indents[:-deferredDedents]
                        deferredDedents = 0

                elif code == TAB_CODE:
                    tab = int(item[2:])
                    if column > tab:
                        # User has requested a tab, but the current column is 
                        # beyond the tabstop. There are two different ways this 
                        # can be handled:
                        # 1. either emit a linefeed and then tab to tabstop
                        padding = '\n' + tab * ' '
                        column = tab
                        # 2. or simple add a space
                        #padding = ' '
                        #column += 1
                    else:
                        padding = (tab - column) * ' '
                        column = tab
                    line += padding
                elif code == KILL_CODE:
                    raise AssertionError
                elif code == INDENT_CODE:
                    levels = int(item[2:])
                    indents += levels*[unitIndent]
                    column = sum([len(indent) for indent in indents])
                elif code == LEADER_CODE:
                    key = item[2:]
                    indents += [leaders[key]]
                elif code == DEDENT_CODE:
                    levels = int(item[2:])
                    assert len(indents) >= levels, 'too many dedents'
                    indents = indents[:-levels]
                elif code == DEFERRED_INDENT_CODE:
                    levels = int(item[2:])
                    deferredIndents += levels*[unitIndent]
                elif code == DEFERRED_LEADER_CODE:
                    key = item[2:]
                    deferredIndents += [leaders[key]]
                elif code == DEFERRED_DEDENT_CODE:
                    levels = int(item[2:])
                    deferredDedents += levels
                elif code == EMPTY_CODE:
                    assert item == EMPTY_CODE
                else:
                    raise AssertionError("ERROR: Unknown control code <%s>" % code[1:])
            else:
                assert '\n' not in item, "rules must not contains newlines"
                if newlines:
                    yield '\n' + (newlines-1)*(''.join(indents).rstrip() + '\n')
                column += len(item)
                line += item
                newlines = 0
                prev_newline_was_soft = False
        assert len(indents) == deferredDedents, 'dedent missing'
        if newlines:
            yield '\n' + (newlines-1)*(''.join(indents).rstrip() + '\n')
        yield line

    # _eligibleToBeRule() {{{2
    @staticmethod
    def _eligibleToBeRule(rule):
        # Eligible to be a rule
        # Returns true if argument is a valid rule for Production.
        # Is a static method because rule may not be instance of Production.
        return (
            rule is None or
            is_str(rule) or
            isinstance(rule, Production) or
            hasattr(rule, 'docgen_rule') or
            hasattr(rule, 'docgen_templet') or
            hasattr(rule, 'docgen_template') or
            (   is_iterable(rule) and
                all([Production._eligibleToBeRule(r) for r in rule])
            )
        )

    # render() {{{2
    def render(
        self,
        indent=DEFAULT_INDENT,
        leaders=None,
        terminate=False
    ):
        """Convert production to a string

        :param indent:
            Indent productions will add this string to beginning of each line.
            If not specified, the indent is four spaces.

        :param leader:
            A dictionary of leaders. Leader productions will choose from these 
            leaders using the corresponding keys. If not specified, the default 
            leaders are used, which include '*' (maps to ' * ' and is used to 
            build C-style multiline comments), '#' (maps to '# ' and is used to 
            build Python style comments), and '//' (maps to '// ' and is used 
            for C++ style comments).
        :param terminate:
            A boolean that defaults to False. If true, a newline will be added 
            to the end of the output (normally that last newline is removed).
        :rtype: A string.
        """
        if leaders is None:
            leaders = DEFAULT_LEADERS
        return (
            strip_trailing_whitespace(''.join(self._filter(indent, leaders))) +
            ('\n' if terminate else '')
        )

    # __repr__() {{{2
    try:
        def __repr__(self):
            from kskutils import toStr
            return toStr(self)
    except ImportError:
        pass

    # __str__() {{{2
    def __str__(self):
        """Convert production to a string

        Use default indent and leaders.
        """
        return self.render()

    # expand() {{{2
    # make .expand() an alias for .render() for backward compatility
    expand = render

    # __bool__() {{{2
    def __bool__(self):
        return self.rule is not None

    # define __nonzero__ for backward compatibility with python2
    __nonzero__ = __bool__

# render {{{1
def render(arg):
    """Render production

    This intended only for debugging. It converts a production or a production 
    compatible object to a string.
    """
    return Production(arg).render()
