#!/usr/bin/env python3
# -*- Mode: Python; py-indent-offset: 4 -*-


import string
from io import StringIO

class error(Exception):
    def __init__(self, filename, lineno, msg):
        Exception.__init__(self, msg)
        self.filename = filename
        self.lineno = lineno
        self.msg = msg
    def __str__(self):
        return '%s:%d: error: %s' % (self.filename, self.lineno, self.msg)

trans = [' '] * 256
for i in range(256):
    if chr(i) in string.ascii_letters + string.digits + '_':
        trans[i] = chr(i)
    else:
        trans[i] = '_'
trans = ''.join(trans)

def parse(filename):
    if isinstance(filename, str):
        fp = open(filename, 'r')
    else: # if not string, assume it is some kind of iterator
        fp = filename
        filename = getattr(fp, 'name', '<unknown>')
    whitespace = ' \t\n\r\x0b\x0c'
    nonsymbol = whitespace + '();\'"'
    stack = []
    openlines = []
    lineno = 0
    for line in fp:
        pos = 0
        lineno += 1
        while pos < len(line):
            if line[pos] in whitespace: # ignore whitespace
                pass
            elif line[pos] == ';': # comment
                break
            elif line[pos:pos+2] == "'(":
                pass # the open parenthesis will be handled next iteration
            elif line[pos] == '(':
                stack.append(())
                openlines.append(lineno)
            elif line[pos] == ')':
                if len(stack) == 0:
                    raise error(filename, lineno, 'close parenthesis found when none open')
                closed = stack[-1]
                del stack[-1]
                del openlines[-1]
                if stack:
                    stack[-1] += (closed,)
                else:
                    yield closed
            elif line[pos] == '"': # quoted string
                if not stack:
                    raise error(filename, lineno,
                                'string found outside of s-expression')
                endpos = pos + 1
                chars = []
                while endpos < len(line):
                    if endpos+1 < len(line) and line[endpos] == '\\':
                        endpos += 1
                        if line[endpos] == 'n':
                            chars.append('\n')
                        elif line[endpos] == 'r':
                            chars.append('\r')
                        elif line[endpos] == 't':
                            chars.append('\t')
                        else:
                            chars.append('\\')
                            chars.append(line[endpos])
                    elif line[endpos] == '"':
                        break
                    else:
                        chars.append(line[endpos])
                    endpos += 1
                if endpos >= len(line):
                    raise error(filename, lineno, "unclosed quoted string")
                pos = endpos
                stack[-1] += (''.join(chars),)
            else: # symbol/number
                if not stack:
                    raise error(filename, lineno,
                                'identifier found outside of s-expression')
                endpos = pos
                while endpos < len(line) and line[endpos] not in nonsymbol:
                    endpos += 1
                symbol = line[pos:endpos]
                pos = max(pos, endpos-1)
                try: symbol = int(symbol)
                except ValueError:
                    try: symbol = float(symbol)
                    except ValueError: pass
                stack[-1] += (symbol,)
            pos += 1
    if len(stack) != 0:
        msg = '%d unclosed parentheses found at end of ' \
              'file (opened on line(s) %s)' % (len(stack),
                                               ', '.join(map(str, openlines)))
        raise error(filename, lineno, msg)

class Parser:
    def __init__(self, filename):
        """Argument is either a string, a parse tree, or file object"""
        self.filename = filename
    def startParsing(self, filename=None):
        statements = parse(filename or self.filename)
        for statement in statements:
            self.handle(statement)
    def handle(self, tup):
        cmd = tup[0].translate(trans)
        if hasattr(self, cmd):
            getattr(self, cmd)(*tup[1:])
        else:
            self.unknown(tup)
    def unknown(self, tup):
        pass

_testString = """; a scheme file
(define-func gdk_font_load    ; a comment at end of line
  GdkFont
  ((string name)))

(define-boxed GdkEvent
  gdk_event_copy
  gdk_event_free
  "sizeof(GdkEvent)")
"""

if __name__ == '__main__':
    import sys
    if sys.argv[1:]:
        fp = open(sys.argv[1])
    else:
        fp = StringIO(_testString)
    statements = parse(fp)
    for s in statements:
        print(repr(s))
