""" This file contains parsing routines and object classes to help derive meaning from
raw lists of tokens from pyparsing. """

import abc
import logging

import six

from acme.magic_typing import List
from certbot import errors

logger = logging.getLogger(__name__)
COMMENT = " managed by Certbot"
COMMENT_BLOCK = ["#", COMMENT]


class Parsable(object):
    """ Abstract base class for "Parsable" objects whose underlying representation
    is a tree of lists.

    :param .Parsable parent: This object's parsed parent in the tree
    """

    __metaclass__ = abc.ABCMeta

    def __init__(self, parent=None):
        self._data = [] # type: List[object]
        self._tabs = None
        self.parent = parent

    @classmethod
    def parsing_hooks(cls):
        """Returns object types that this class should be able to `parse` recusrively.
        The order of the objects indicates the order in which the parser should
        try to parse each subitem.
        :returns: A list of Parsable classes.
        :rtype list:
        """
        return (Block, Sentence, Statements)

    @staticmethod
    @abc.abstractmethod
    def should_parse(lists):
        """ Returns whether the contents of `lists` can be parsed into this object.

        :returns: Whether `lists` can be parsed as this object.
        :rtype bool:
        """
        raise NotImplementedError()

    @abc.abstractmethod
    def parse(self, raw_list, add_spaces=False):
        """ Loads information into this object from underlying raw_list structure.
        Each Parsable object might make different assumptions about the structure of
        raw_list.

        :param list raw_list: A list or sublist of tokens from pyparsing, containing whitespace
            as separate tokens.
        :param bool add_spaces: If set, the method can and should manipulate and insert spacing
            between non-whitespace tokens and lists to delimit them.
        :raises .errors.MisconfigurationError: when the assumptions about the structure of
            raw_list are not met.
        """
        raise NotImplementedError()

    @abc.abstractmethod
    def iterate(self, expanded=False, match=None):
        """ Iterates across this object. If this object is a leaf object, only yields
        itself. If it contains references other parsing objects, and `expanded` is set,
        this function should first yield itself, then recursively iterate across all of them.
        :param bool expanded: Whether to recursively iterate on possible children.
        :param callable match: If provided, an object is only iterated if this callable
            returns True when called on that object.

        :returns: Iterator over desired objects.
        """
        raise NotImplementedError()

    @abc.abstractmethod
    def get_tabs(self):
        """ Guess at the tabbing style of this parsed object, based on whitespace.

        If this object is a leaf, it deducts the tabbing based on its own contents.
        Other objects may guess by calling `get_tabs` recursively on child objects.

        :returns: Guess at tabbing for this object. Should only return whitespace strings
            that does not contain newlines.
        :rtype str:
        """
        raise NotImplementedError()

    @abc.abstractmethod
    def set_tabs(self, tabs="    "):
        """This tries to set and alter the tabbing of the current object to a desired
        whitespace string. Primarily meant for objects that were constructed, so they
        can conform to surrounding whitespace.

        :param str tabs: A whitespace string (not containing newlines).
        """
        raise NotImplementedError()

    def dump(self, include_spaces=False):
        """ Dumps back to pyparsing-like list tree. The opposite of `parse`.

        Note: if this object has not been modified, `dump` with `include_spaces=True`
        should always return the original input of `parse`.

        :param bool include_spaces: If set to False, magically hides whitespace tokens from
            dumped output.

        :returns: Pyparsing-like list tree.
        :rtype list:
        """
        return [elem.dump(include_spaces) for elem in self._data]


class Statements(Parsable):
    """ A group or list of "Statements". A Statement is either a Block or a Sentence.

    The underlying representation is simply a list of these Statement objects, with
    an extra `_trailing_whitespace` string to keep track of the whitespace that does not
    precede any more statements.
    """
    def __init__(self, parent=None):
        super(Statements, self).__init__(parent)
        self._trailing_whitespace = None

    # ======== Begin overridden functions

    @staticmethod
    def should_parse(lists):
        return isinstance(lists, list)

    def set_tabs(self, tabs="    "):
        """ Sets the tabbing for this set of statements. Does this by calling `set_tabs`
        on each of the child statements.

        Then, if a parent is present, sets trailing whitespace to parent tabbing. This
        is so that the trailing } of any Block that contains Statements lines up
        with parent tabbing.
        """
        for statement in self._data:
            statement.set_tabs(tabs)
        if self.parent is not None:
            self._trailing_whitespace = "\n" + self.parent.get_tabs()

    def parse(self, raw_list, add_spaces=False):
        """ Parses a list of statements.
        Expects all elements in `raw_list` to be parseable by `type(self).parsing_hooks`,
        with an optional whitespace string at the last index of `raw_list`.
        """
        if not isinstance(raw_list, list):
            raise errors.MisconfigurationError("Statements parsing expects a list!")
        # If there's a trailing whitespace in the list of statements, keep track of it.
        if raw_list and isinstance(raw_list[-1], six.string_types) and raw_list[-1].isspace():
            self._trailing_whitespace = raw_list[-1]
            raw_list = raw_list[:-1]
        self._data = [parse_raw(elem, self, add_spaces) for elem in raw_list]

    def get_tabs(self):
        """ Takes a guess at the tabbing of all contained Statements by retrieving the
        tabbing of the first Statement."""
        if self._data:
            return self._data[0].get_tabs()
        return ""

    def dump(self, include_spaces=False):
        """ Dumps this object by first dumping each statement, then appending its
        trailing whitespace (if `include_spaces` is set) """
        data = super(Statements, self).dump(include_spaces)
        if include_spaces and self._trailing_whitespace is not None:
            return data + [self._trailing_whitespace]
        return data

    def iterate(self, expanded=False, match=None):
        """ Combines each statement's iterator.  """
        for elem in self._data:
            for sub_elem in elem.iterate(expanded, match):
                yield sub_elem

    # ======== End overridden functions


def _space_list(list_):
    """ Inserts whitespace between adjacent non-whitespace tokens. """
    spaced_statement = [] # type: List[str]
    for i in reversed(six.moves.xrange(len(list_))):
        spaced_statement.insert(0, list_[i])
        if i > 0 and not list_[i].isspace() and not list_[i-1].isspace():
            spaced_statement.insert(0, " ")
    return spaced_statement


class Sentence(Parsable):
    """ A list of words. Non-whitespace words are typically separated with whitespace tokens. """

    # ======== Begin overridden functions

    @staticmethod
    def should_parse(lists):
        """ Returns True if `lists` can be parseable as a `Sentence`-- that is,
        every element is a string type.

        :param list lists: The raw unparsed list to check.

        :returns: whether this lists is parseable by `Sentence`.
        """
        return isinstance(lists, list) and len(lists) > 0 and \
            all(isinstance(elem, six.string_types) for elem in lists)

    def parse(self, raw_list, add_spaces=False):
        """ Parses a list of string types into this object.
        If add_spaces is set, adds whitespace tokens between adjacent non-whitespace tokens."""
        if add_spaces:
            raw_list = _space_list(raw_list)
        if not isinstance(raw_list, list) or \
                any(not isinstance(elem, six.string_types) for elem in raw_list):
            raise errors.MisconfigurationError("Sentence parsing expects a list of string types.")
        self._data = raw_list

    def iterate(self, expanded=False, match=None):
        """ Simply yields itself. """
        if match is None or match(self):
            yield self

    def set_tabs(self, tabs="    "):
        """ Sets the tabbing on this sentence. Inserts a newline and `tabs` at the
        beginning of `self._data`. """
        if self._data[0].isspace():
            return
        self._data.insert(0, "\n" + tabs)

    def dump(self, include_spaces=False):
        """ Dumps this sentence. If include_spaces is set, includes whitespace tokens."""
        if not include_spaces:
            return self.words
        return self._data

    def get_tabs(self):
        """ Guesses at the tabbing of this sentence. If the first element is whitespace,
        returns the whitespace after the rightmost newline in the string. """
        first = self._data[0]
        if not first.isspace():
            return ""
        rindex = first.rfind("\n")
        return first[rindex+1:]

    # ======== End overridden functions

    @property
    def words(self):
        """ Iterates over words, but without spaces. Like Unspaced List. """
        return [word.strip("\"\'") for word in self._data if not word.isspace()]

    def __getitem__(self, index):
        return self.words[index]

    def __contains__(self, word):
        return word in self.words


class Block(Parsable):
    """ Any sort of bloc, denoted by a block name and curly braces, like so:
    The parsed block:
        block name {
            content 1;
            content 2;
        }
    might be represented with the list [names, contents], where
        names = ["block", " ", "name", " "]
        contents = [["\n    ", "content", " ", "1"], ["\n    ", "content", " ", "2"], "\n"]
    """
    def __init__(self, parent=None):
        super(Block, self).__init__(parent)
        self.names = None # type: Sentence
        self.contents = None # type: Block

    @staticmethod
    def should_parse(lists):
        """ Returns True if `lists` can be parseable as a `Block`-- that is,
        it's got a length of 2, the first element is a `Sentence` and the second can be
        a `Statements`.

        :param list lists: The raw unparsed list to check.

        :returns: whether this lists is parseable by `Block`. """
        return isinstance(lists, list) and len(lists) == 2 and \
            Sentence.should_parse(lists[0]) and isinstance(lists[1], list)

    def set_tabs(self, tabs="    "):
        """ Sets tabs by setting equivalent tabbing on names, then adding tabbing
        to contents."""
        self.names.set_tabs(tabs)
        self.contents.set_tabs(tabs + "    ")

    def iterate(self, expanded=False, match=None):
        """ Iterator over self, and if expanded is set, over its contents. """
        if match is None or match(self):
            yield self
        if expanded:
            for elem in self.contents.iterate(expanded, match):
                yield elem

    def parse(self, raw_list, add_spaces=False):
        """ Parses a list that resembles a block.

        The assumptions that this routine makes are:
            1. the first element of `raw_list` is a valid Sentence.
            2. the second element of `raw_list` is a valid Statement.
        If add_spaces is set, we call it recursively on `names` and `contents`, and
        add an extra trailing space to `names` (to separate the block's opening bracket
        and the block name).
        """
        if not Block.should_parse(raw_list):
            raise errors.MisconfigurationError("Block parsing expects a list of length 2. "
                "First element should be a list of string types (the bloc names), "
                "and second should be another list of statements (the bloc content).")
        self.names = Sentence(self)
        if add_spaces:
            raw_list[0].append(" ")
        self.names.parse(raw_list[0], add_spaces)
        self.contents = Statements(self)
        self.contents.parse(raw_list[1], add_spaces)
        self._data = [self.names, self.contents]

    def get_tabs(self):
        """ Guesses tabbing by retrieving tabbing guess of self.names. """
        return self.names.get_tabs()

def _is_comment(parsed_obj):
    """ Checks whether parsed_obj is a comment.

    :param .Parsable parsed_obj:

    :returns: whether parsed_obj represents a comment sentence.
    :rtype bool:
    """
    if not isinstance(parsed_obj, Sentence):
        return False
    return parsed_obj.words[0] == "#"

def _is_certbot_comment(parsed_obj):
    """ Checks whether parsed_obj is a "managed by Certbot" comment.

    :param .Parsable parsed_obj:

    :returns: whether parsed_obj is a "managed by Certbot" comment.
    :rtype bool:
    """
    if not _is_comment(parsed_obj):
        return False
    if len(parsed_obj.words) != len(COMMENT_BLOCK):
        return False
    for i, word in enumerate(parsed_obj.words):
        if word != COMMENT_BLOCK[i]:
            return False
    return True

def _certbot_comment(parent, preceding_spaces=4):
    """ A "Managed by Certbot" comment.
    :param int preceding_spaces: Number of spaces between the end of the previous
        statement and the comment.
    :returns: Sentence containing the comment.
    :rtype: .Sentence
    """
    result = Sentence(parent)
    result.parse([" " * preceding_spaces] + COMMENT_BLOCK)
    return result

def _choose_parser(parent, list_):
    """ Choose a parser from type(parent).parsing_hooks, depending on whichever hook
    returns True first. """
    hooks = Parsable.parsing_hooks()
    if parent:
        hooks = type(parent).parsing_hooks()
    for type_ in hooks:
        if type_.should_parse(list_):
            return type_(parent)
    raise errors.MisconfigurationError(
        "None of the parsing hooks succeeded, so we don't know how to parse this set of lists.")

def parse_raw(lists_, parent=None, add_spaces=False):
    """ Primary parsing factory function.

    :param list lists_: raw lists from pyparsing to parse.
    :param .Parent parent: The parent containing this object.
    :param bool add_spaces: Whether to pass add_spaces to the parser.

    :returns .Parsable: The parsed object.

    :raises errors.MisconfigurationError: If no parsing hook passes, and we can't
        determine which type to parse the raw lists into.
    """
    parser = _choose_parser(parent, lists_)
    parser.parse(lists_, add_spaces)
    return parser
