#!/usr/bin/python2.7
"""Parser Modules

From Exercise 49 of 'Learn Python the Hard Way', 3rd Ed.
"""

### IMPORTS ###################################################################
from ex48.lexicon import scan


### FUNCTIONS #################################################################
def pop_until_key(lists, keys):
    """
    Pop items off a list until the first element of the popped item is
    in the list of keys.

    Arguments:
        - lists: a list of sliceable objects (ie. list, tuple, or array)
        - keys: a list of keys that can terminate the popping

    Returns:
        - if lists is None, return None
        - elif one of the keys can be found, return the popped item
        - else return None

    Examples:
        >>> pop_until_key(None, 'x') is None
        True

        >>> pop_until_key(['aa12', 'bb34', 'cc56', 'dd78'], 'c')
        'cc56'

        >>> pop_until_key([('aa', 12),('ab', 34)], 'ab')
        ('ab', 34)

        >>> pop_until_key([['ele0', 'ele1'], ['eleA', 'eleB']], 'eleA')
        ['eleA', 'eleB']

        >>> pop_until_key([['ele0', 'ele1'], ['eleA', 'eleB']], 'eleB') is None
        True

        >>> pop_until_key([['ele0', 'ele1'], ['eleA', 'eleB']], None)
        Traceback (most recent call last):
            ...
        TypeError: The keys argument of pop_until_keys cannot be None

    """

    if lists is None:
        return None
    if keys is None:
        raise TypeError('The keys argument of pop_until_keys cannot be None')

    while len(lists) > 0:
        popped = lists.pop(0)
        if popped[0] in keys:
            return popped


def parse(lexicon_tuples):
    """Parse a list of tuples from left to right for Subject-Verb-Object

    The purpose of this function is to a list of tuples (assumed to be
    generated by the 'scan' function) in order to determine the meaning
    of a sentence.
    It is also assumed that the user is directing an avatar of
    themselves. Therefore commands are interpreted with a 1st person
    subject.
    eg. 'Go North'  -> Player go North

    Arguments:
        - lexicon_tuples: a list of tuples; assumed to be generated
            by the scan function

    Returns:
        - a dict with three keys: 'subject', 'verb, 'object'

    Examples:
        # Original Sentence: 'I open the door'
        >>> input = [('noun', 'I'), ('verb', 'open'), ('stop', 'the'), ('noun', 'door')]
        >>> expected = {'subject': 'I', 'verb': 'open', 'object': 'door'}
        >>> parse(input) == expected
        True

        # Original Sentence: 'go north'
        >>> expected = {'subject': 'player', 'verb': 'go', 'object': 'north'}
        >>> parse([('verb', 'go'), ('direction', 'north')]) == expected
        True

        # Original Sentence: 'foo bar baz'
        >>> expected = {'subject': None, 'verb': None, 'object': None}
        >>> parse([('error', 'foo'), ('error', 'bar'), ('error', 'baz')]) == expected
        True

        # Original Sentence: 'I I I'
        >>> expected = {'subject': 'I', 'verb': None, 'object': None}
        >>> parse([('noun', 'I'), ('noun', 'I'), ('noun', 'I')]) == expected
        True

        # Original Sentence: 'I run'
        >>> expected = {'subject': 'I', 'verb': 'run', 'object': None}
        >>> parse([('noun', 'I'), ('verb', 'run')]) == expected
        True

    """

    subject = pop_until_key(lists=lexicon_tuples, keys=['noun', 'verb'])
    # If the subject is implied by the verb (eg 'Open the door')
    # un-pop the pop-ed tuple and set the subject manually
    if subject is not None and subject[0] == 'verb':
        lexicon_tuples.insert(0, subject)
        subject = ('noun', 'player')

    verb = pop_until_key(lists=lexicon_tuples, keys=['verb'])
    obj = pop_until_key(lists=lexicon_tuples, keys=['noun', 'direction'])

    return {
        'subject': subject[1] if subject is not None else None,
        'verb': verb[1] if verb is not None else None,
        'object': obj[1] if obj is not None else None,
    }


### CLASSES ###################################################################
class Parser(object):
    """Sentence object composed of a sentence subject, a verb, and an object"""

    ### INIT ##################################################################
    def __init__(self, value):
        """Init the sentence by passing in the plaintext sentence"""
        self.plaintext = value

    ### PROPERTIES ############################################################
    @property
    def tokenized(self):
        """Return a copy that scan has broken into token/word tuples"""
        return scan(self.plaintext)

    @property
    def parsed(self):
        """Return a dict of the parsed sentence components"""
        return parse(self.tokenized)

    @property
    def subject(self):
        """Return the parsed subject from the sentence"""
        return self.parsed['subject']

    @property
    def verb(self):
        """Return the parsed verb from the sentence"""
        return self.parsed['verb']

    @property
    def object(self):
        """Return the parsed object from the sentence"""
        return self.parsed['object']


### END OF EXERCISE. ##################################################
# pylint: disable = W0105
"""Study Drills

Q1: Change the parse_ methods and try to put them into a class rather
    than be just methods. Which design do you like better?
A1: I tried it, but disliked it because it then prevents you from
    calling the functions in unexpected ways - limiting reuse.
    Eg. Adding a new token to the lexicon and searching for it using
        the existing function.
    I did like how it made it easy to use the code without knowing much
    about it.
    As a 'best of both worlds' solution, I set up the class to provide
    access to the functions via class properties. That way, you can
    either use:
        parse('I run away')
        or
        Parser('I run away').parsed

Q2: Make the parser more error-resistant so that you can avoid annoying
    your users if they type words your lexicon doesn't understand.
A2: I made it ignore error tokens and return None when it runs out of
    tuples.
    This kicks the problem down the road to whoever is using the
    function, so they can decide how to handle the Nones.

Q3: Improve the grammar by handling more things like numbers.
A3: I did? The 'pop_until_key' function allows any sort of token to be
    searched for. If the programmer needs something more elaborate than
    parse, they can build it using pop_until_key. I can't solve every
    future problem while designing a function, so I just design it to
    be accesible for future problems.

Q4: Think about how you might use this Sentence class in your game to
    do more fun things with a user's input.
A4: It makes for a more interactive, less frustrating experience.

    Instead of having to guess the exact input that the programmer
    coded for, you can just type in what you intend to do and will have
    a reasonable chance of the program understanding you.

    Since the program is now much less finicky about what inputs are
    needed, the programmer can now make use of much more varied
    responses without stressing as much about 'Will they phrase this
    exactly how I did?'
"""
