# coding=utf-8
import re
import mal_types as mal


class Reader:
    """A Reader object for storing a list of tokens and an index into that list.

    """

    def __init__(self, tokens):
        self.tokens = tokens
        self.position = 0

    def next(self):
        """Return the current token and increment the index.

        If the list of tokens has been exausted, return the empty string.

        """
        token = self.peek()
        self.position += 1
        return token

    def peek(self):
        """Return the current token.

        If the list of tokens has been exhausted, return the empty string.
        """
        if self.position >= len(self.tokens):
            return ''
        else:
            return self.tokens[self.position]


reader_macros = {"'": "quote",
                 "`": "quasiquote",
                 "~": "unquote",
                 "~@": "splice-unquote",
                 "@": "deref"}


def read_str(input_str):
    """Convert INPUT_STR into a Mal object."""
    tokens = tokenize(input_str)
    mal_object = read_form(Reader(tokens))
    return mal_object


def tokenize(input_str):
    """Tokenize INPUT_STR.

    Return a list of tokens."""
    token_regexp = (r'[\s,]*'
                    r'(~@|'
                    r'[\[\]{}()\'`~^@]|'
                    r'"(?:\\.|[^\\"])*"'
                    r'|;.*|'
                    r'[^\s\[\]{}(\'"`,;)]*)')

    tokens = re.findall(token_regexp, input_str)

    # The re.findall() call adds an empty match to the end of the list. I'm not
    # sure how to remove this other than by checking for it explictly. We also
    # filter out comments at this point.
    return [token for token in tokens if token != '' and token[0] != ';']


def read_form(form):
    token = form.next()
    if token in ['(', '[', '{']:
        return read_sequence(form, token)
    elif token == '^':  # with-meta reader macro
        return apply_with_meta_macro(form)
    elif token in reader_macros:
        return apply_reader_macro(form, token)
    elif token == '':
        return None
    else:
        return read_atom(token)


def read_sequence(form, token):
    """Read a sequence from FORM.

    This function reads list, vectors and hash tables.
    """
    res = []

    end_token = {'(': ')', '[': ']', '{': '}'}[token]

    while True:
        token = form.peek()
        if token == end_token:  # We've found the end of the list.
            break
        if token == '':  # We've reached the end of FORM.
            return mal.Error("ParenError", "Missing closing parenthesis")
        next_form = read_form(form)
        if type(next_form) is mal.Error:
            return next_form

        res.append(next_form)

    # Now we need to move past the end token
    form.next()

    if end_token == ')':
        return mal.List(res)
    elif end_token == '}':
        return create_hash(res)
    else:
        return mal.Vector(res)


def create_hash(items):
    """Create a hash table from ITEMS."""

    # Hash tables in Mal can have strings or keywords as keys. mal.Keyword are
    # hashable, so there's no need to use a rare Unicode character as prefix in
    # order to distinguish them from strings, as suggested in the mal_guide.

    if (len(items) % 2) != 0:
        return mal.Error("HashError", "Insufficient number of items")

    res = {}
    for i in range(0, len(items), 2):
        key = items[i]
        if not isinstance(key, (str, mal.Keyword)):
            return mal.Error("HashError",
                             "Cannot hash on {}".format(type(key)))
        value = items[i + 1]
        res[key] = value
    return mal.Hash(res)


def apply_with_meta_macro(form):
    data = read_form(form)
    if type(data) is mal.Error:
        return data
    obj = read_form(form)
    if type(obj) is mal.Error:
        return obj
    return mal.List([mal.Symbol('with-meta'), obj, data])


def apply_reader_macro(form, token):
    next_form = read_form(form)
    if type(next_form) is mal.Error:
        return next_form
    replacement = mal.Symbol(reader_macros[token])
    return mal.List([replacement, next_form])


def read_atom(token):
    # integers
    if re.match(r'\A-?[0-9]+\Z', token):
        return int(token)

    # strings
    if re.match(r'\A"(.*)"\Z', token):
        string = token[1:-1]
        string = string.replace(r'\"', '"')
        string = string.replace(r'\n', '\n')
        string = string.replace(r'\\', '\\')
        return string

    # keywords
    if re.match(r'\A:.*\Z', token):
        return mal.Keyword(token)

    # boolean
    if token == "true":
        return mal.Boolean(True)
    if token == "false":
        return mal.Boolean(False)

    # nil
    if token == "nil":
        return mal.NIL

    # symbols
    if re.match(r"[^\s\[\]{}('\"`,;)]*", token):
        return mal.Symbol(token)

    # Found nothing parsable. (Shouldn't really happen, since symbols are a
    # catch-all already.)
    return mal.Error("ParseError", "Could not parse token: '{}'".
                     format(token))


def main():
    form = '(def (fn a (b c)) (print (+ a b)))'
    print(read_str(form))


if __name__ == '__main__':
    main()

# coding=utf-8
import re
import mal_types as mal


class Reader:
    """A Reader object for storing a list of tokens and an index into that list.

    """

    def __init__(self, tokens):
        self.tokens = tokens
        self.position = 0

    def next(self):
        """Return the current token and increment the index.

        If the list of tokens has been exausted, return the empty string.

        """
        token = self.peek()
        self.position += 1
        return token

    def peek(self):
        """Return the current token.

        If the list of tokens has been exhausted, return the empty string.
        """
        if self.position >= len(self.tokens):
            return ''
        else:
            return self.tokens[self.position]


reader_macros = {"'": "quote",
                 "`": "quasiquote",
                 "~": "unquote",
                 "~@": "splice-unquote",
                 "@": "deref"}


def read_str(input_str):
    """Convert INPUT_STR into a Mal object."""
    tokens = tokenize(input_str)
    mal_object = read_form(Reader(tokens))
    return mal_object


def tokenize(input_str):
    """Tokenize INPUT_STR.

    Return a list of tokens."""
    token_regexp = (r'[\s,]*'
                    r'(~@|'
                    r'[\[\]{}()\'`~^@]|'
                    r'"(?:\\.|[^\\"])*"'
                    r'|;.*|'
                    r'[^\s\[\]{}(\'"`,;)]*)')

    tokens = re.findall(token_regexp, input_str)

    # The re.findall() call adds an empty match to the end of the list. I'm not
    # sure how to remove this other than by checking for it explictly. We also
    # filter out comments at this point.
    return [token for token in tokens if token != '' and token[0] != ';']


def read_form(form):
    token = form.next()
    if token in ['(', '[', '{']:
        return read_sequence(form, token)
    elif token == '^':  # with-meta reader macro
        return apply_with_meta_macro(form)
    elif token in reader_macros:
        return apply_reader_macro(form, token)
    elif token == '':
        return None
    else:
        return read_atom(token)


def read_sequence(form, token):
    """Read a sequence from FORM.

    This function reads list, vectors and hash tables.
    """
    res = []

    end_token = {'(': ')', '[': ']', '{': '}'}[token]

    while True:
        token = form.peek()
        if token == end_token:  # We've found the end of the list.
            break
        if token == '':  # We've reached the end of FORM.
            return mal.Error("ParenError", "Missing closing parenthesis")
        next_form = read_form(form)
        if type(next_form) is mal.Error:
            return next_form

        res.append(next_form)

    # Now we need to move past the end token
    form.next()

    if end_token == ')':
        return mal.List(res)
    elif end_token == '}':
        return create_hash(res)
    else:
        return mal.Vector(res)


def create_hash(items):
    """Create a hash table from ITEMS."""

    # Hash tables in Mal can have strings or keywords as keys. mal.Keyword are
    # hashable, so there's no need to use a rare Unicode character as prefix in
    # order to distinguish them from strings, as suggested in the mal_guide.

    if (len(items) % 2) != 0:
        return mal.Error("HashError", "Insufficient number of items")

    res = {}
    for i in range(0, len(items), 2):
        key = items[i]
        if not isinstance(key, (str, mal.Keyword)):
            return mal.Error("HashError",
                             "Cannot hash on {}".format(type(key)))
        value = items[i + 1]
        res[key] = value
    return mal.Hash(res)


def apply_with_meta_macro(form):
    data = read_form(form)
    if type(data) is mal.Error:
        return data
    obj = read_form(form)
    if type(obj) is mal.Error:
        return obj
    return mal.List([mal.Symbol('with-meta'), obj, data])


def apply_reader_macro(form, token):
    next_form = read_form(form)
    if type(next_form) is mal.Error:
        return next_form
    replacement = mal.Symbol(reader_macros[token])
    return mal.List([replacement, next_form])


def read_atom(token):
    # integers
    if re.match(r'\A-?[0-9]+\Z', token):
        return int(token)

    # strings
    if re.match(r'\A"(.*)"\Z', token):
        string = token[1:-1]
        string = string.replace(r'\"', '"')
        string = string.replace(r'\n', '\n')
        string = string.replace(r'\\', '\\')
        return string

    # keywords
    if re.match(r'\A:.*\Z', token):
        return mal.Keyword(token)

    # boolean
    if token == "true":
        return mal.Boolean(True)
    if token == "false":
        return mal.Boolean(False)

    # nil
    if token == "nil":
        return mal.NIL

    # symbols
    if re.match(r"[^\s\[\]{}('\"`,;)]*", token):
        return mal.Symbol(token)

    # Found nothing parsable. (Shouldn't really happen, since symbols are a
    # catch-all already.)
    return mal.Error("ParseError", "Could not parse token: '{}'".
                     format(token))


def main():
    form = '(def (fn a (b c)) (print (+ a b)))'
    print(read_str(form))


if __name__ == '__main__':
    main()

# coding=utf-8 A Reader object for storing a list of tokens and an index into that list. Return the current token and increment the index.          If the list of tokens has been exausted, return the empty string. Return the current token.          If the list of tokens has been exhausted, return the empty string. Convert INPUT_STR into a Mal object. Tokenize INPUT_STR.      Return a list of tokens. # The re.findall() call adds an empty match to the end of the list. I'm not # sure how to remove this other than by checking for it explictly. We also # filter out comments at this point. # with-meta reader macro Read a sequence from FORM.      This function reads list, vectors and hash tables. # We've found the end of the list. # We've reached the end of FORM. # Now we need to move past the end token Create a hash table from ITEMS. # Hash tables in Mal can have strings or keywords as keys. mal.Keyword are # hashable, so there's no need to use a rare Unicode character as prefix in # order to distinguish them from strings, as suggested in the mal_guide. # integers # strings # keywords # boolean # nil # symbols # Found nothing parsable. (Shouldn't really happen, since symbols are a # catch-all already.)