#!/usr/bin/python2.6
# Copyright Phil Fritzsche, Sarah Penrose
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# NOTE: Though this is our own work, we did use the LOLPython converter
# located at:
# 
# http://www.dalkescientific.com/writings/diary/archive/2007/06/01/lolpython.html
#
# as an example. We studied it as a useful implementation and certain design
# choices made here are indeed reminiscent of or occasionally taken from Andrew
# Dalke's work. Kudos to him for getting us started :)

__author__ = 'Phil Fritzsche <pfritzsche@gmail.com>, ' + \
             'Sarah Penrose <sarah.penrose@gmail.com>'
__version__ = 1.0

import keyword
import optparse
import StringIO
import sys
import types

from ply import lex

# ---- Token types ---------------------------------------------------------
# Tuple of token types. PLY uses this to aide in token parsing.
tokens = (
    'NAME', 'RESERVED', 'NUMBER', 'STRING', 'OP',
    'AUTOCALL', 'INLINE', 'COMMENT', 'PRINT',
    'WS', 'NEWLINE', 'CLOSEBLOCK', 'DOCSTRING'
)

# Helpers -- shorten the process of assigning values to given tokens.

def NAME(t, value):
    t.type = 'NAME'
    t.value = value
    return t

def RESERVED(t, value):
    t.type = 'RESERVED'
    t.value = value
    return t

def NUMBER(t, value):
    t.type = 'NUMBER'
    t.value = value
    return t

def DOCSTRING(t, value):
    t.type = 'DOCSTRING'
    t.value = value
    return t

def STRING(t, value):
    t.type = 'STRING'
    t.value = value
    return t

def OP(t, value):
    t.type = 'OP'
    t.value = value
    return t
    
def AUTOCALL(t, value):
    t.type = 'AUTOCALL'
    t.value = value
    t.lexer.paren_stack.append(')')
    return t
    
def INLINE(t, value):
    t.type = 'INLINE'
    t.value = value
    return t

def PRINT(t, value):
    t.type = 'PRINT'
    t.value = value
    return t

# ---- Token regular expressions -------------------------------------------
# The following section of functions is defined as required by PLY. Each
# token match follows the convention t_TOKEN_NAME. The first line of
# each function is a Python regular expression matching the LOLCAT version
# of the token. We then use the above-declared helpers to set the value
# of the supplied token to the Python version of its LOLCAT component.

def t_error(t):
    raise SyntaxError('Unknown symbol %r' % (t.value[0], ))
    t.lexer.skip(1)

def t_WHILEFOREVER(t):
    r'WHILE[ ](?:WUV|CHEEZBURGR)'
    return INLINE(t, 'while True')

def t_AND(t):
    r'AND[ ]DAT\b'
    return RESERVED(t, 'and')

def t_ASSERT(t):
    r'MAEK[ ]SHUR\b'
    return RESERVED(t, 'assert')
    
def t_ASSIGNMENT(t):
    r'CAN[ ]HA[ZS]\b'
    return OP(t, '=')
   
def t_BITAND(t):
    r'BITTY[ ]AND\b'
    return OP(t, '&')

def t_BITOR(t):
    r'BITTY[ ]OR\b'
    return OP(t, '|')

def t_CHR(t):
    r'CHAR\b'
    return AUTOCALL(t, 'chr')

def t_CLASS(t):
    r'OBJEKT[ ]IZ\b'
    return RESERVED(t, 'class')

def t_CLOSEBLOCK(t):
    r'(?:OK(!+|\b)|!+)'
    stack = t.lexer.paren_stack
    num_blocks = t.value.count('!')
    if len(stack) < num_blocks:
        raise ValueError('[%d] Paren stack is missing parens.' % t.lineno)
    # Grab the last {num_blocks} closing delimiters from the stack. Reverse
    # them to insure that they are printed out in proper order.
    t.value = ''.join(stack[-num_blocks:][::-1])
    del stack[-num_blocks:]
    return t

def t_COLON(t):
    r'(?:\?|THRU|AS)'
    return INLINE(t, ':')
    
def t_COMMENT(t):
    r'BTW.*'
    t.type = 'COMMENT'
    return t

def t_DEF(t):
    r'(?:SO[ ]LIEK|REMEMBER[ ]DAT)\b'
    return RESERVED(t, 'def')

def t_DELETE(t):
    r'(?:BAREET|STEEL[ ]BUKKIT|DUZ[ ]NOT[ ]WANT)\b'
    return RESERVED(t, 'del')
    
def t_DICT(t):
    r'NAEMD[ ]BUKKITS\b'
    return INLINE(t, 'dict')

def t_DIVIDE(t):
    r'(?:ESPLODES|BREAKS)[ ]INTO\b'
    return OP(t, '\/')

def t_EQUAL(t):
    r'ZAKLY[ ]LIEK\b'
    return OP(t, '==')
    
def t_ELIF(t):
    r'OR[ ]IZ\b'
    return RESERVED(t, 'elif')
    
def t_ELSE(t):
    r'(?:U[ ]GIV[ ]UP|IZ[ ]KEWL|ELS)\b'
    return RESERVED(t, 'else')
    
def t_EXCEPT(t):
    r'O[ ]NOES\b'
    return RESERVED(t, 'except')

def t_EXP(t):
    r'BOOM\b'
    return OP(t, '**')
    
def t_FALSE(t):
    r'(?:UNTRU|FALS)\b'
    return RESERVED(t, 'False')

def t_FINALLY(t):
    r'DUN[ ]FORGET[!]+\b'
    return RESERVED(t, 'finally')

def t_FOR(t):
    r'FER[ ]EACH\b'
    return RESERVED(t, 'for')
    
def t_FROM(t):
    r'IN[ ]MAI\b'
    return RESERVED(t, 'from')

def t_GT(t):
    r'BIGR[ ]DEN\b'
    return OP(t, '>')

def t_GTE(t):
    r'NO[ ]BIGR[ ]DEN\b'
    return OP(t, '<=')
    
def t_IN(t):
    r'IN[ ](?:UR|THE|THIS|TEH)\b'
    return RESERVED(t, 'in')
    
def t_INPUT(t):
    r'TALK[ ]TO[ ]MEH\b'
    return AUTOCALL(t, 'input')

def t_RAWINPUT(t):
    r'GIV[ ]ME[ ]IT[ ]STRAIT[ ]DOC\b'
    return AUTOCALL(t, 'raw_input')

def t_IS(t):
    r'IZ[ ]ZAKLY\b'
    return RESERVED(t, 'is')
    
def t_LBRACE(t):
    r'SQUIGGLY[ ]BUKKIT\b'
    t.lexer.paren_stack.append('}')
    return OP(t, '{')
    
def t_LBRACKET(t):
    r'BUKKIT(?:[ ]OF)*\b'
    t.lexer.paren_stack.append(']')
    return OP(t, '[')
    
def t_LEN(t):
    r'(?:BIGNESS|HOW[ ]BIG)\b'
    return AUTOCALL(t, 'len')

def t_LOLDOCSTR(t):
    r'ROFL[ ]((?!ROFLCOPTER).|\N)*[ ]ROFLCOPTER'
    return DOCSTRING(t, t.value[5:-11])

def t_LOLSTR(t):
    r'LOL[ ]((?!LOLOL).|\n)*[ ]LOLOL'
    return STRING(t, t.value[4:-6])
    
def t_LPAREN(t):
    r'WIT\b'
    t.lexer.paren_stack.append(')')
    return OP(t, '(')

def t_LT(t):
    r'SMALR[ ]DEN\b'
    return OP(t, '<')

def t_LTE(t):
    r'NO[ ]SMALR[ ]DEN\b'
    return OP(t, '>=')

def t_MINUS(t):
    r'TAEK[ ]AWAY\b'
    return OP(t, '-')

def t_MOD(t):
    r'MODZ\b'
    return OP(t, '%')

def t_MULT(t):
    r'TIEM[SZ]\b'
    return OP(t, '*')

def t_NEQUAL(t):
    r'NOT[ ]AT[ ]ALL[ ]LIEK\b'
    return OP(t, '!=')

def t_NEWLINE(t):
    r'\n+'
    t.lexer.lineno += len(t.value)
    t.type = "NEWLINE"
    if not t.lexer.paren_stack:
        return t
   
def t_NONE(t):
    r'NUN[ ]LOL\b'
    return RESERVED(t, 'None')

def t_NOT(t):
    r'(?:AINT|IDNT)\b'
    return RESERVED(t, 'not')

def t_NUMBER(t):
    r'\d+'
    return NUMBER(t, t.value)

def t_OR(t):
    r'(?:MEBBE|OR[ ](?:DIS|MEBBE))\b'
    return RESERVED(t, 'or')

def t_PASS(t):
    r'SKIPPIT\b'
    return RESERVED(t, 'pass')

def t_PRINT(t):
    r'(?:MAEK[ ]VIZIBLES|DISPLAYZ)\b'
    return PRINT(t, 'stdout')

def t_PRINT_ERROR(t):
    r'MAEK[ ]UH[ ]OHZ[ ]KNOWN[1!]*\b'
    return PRINT(t, 'stderr')
    
def t_PLUS(t):
    r'(?:PLU[SZ]|ALONG[ ]WIT)\b'
    return OP(t, '+')
    
def t_RAISE(t):
    r'U[ ]BROKD[ ]IT[1!]+'
    return RESERVED(t, 'raise')

def t_RETURN(t):
    r'(?:GIV[ ]EM|U[ ]CAN[ ]HA[ZS]|U[ ]TAEK)\b'
    return RESERVED(t, 'return')
    
def t_TRUE(t):
    r'(?:TRU|SO[ ]RITE)\b'
    return RESERVED(t, 'True')

def t_WS(t):
    r' [ ]+'
    if not t.lexer.paren_stack:
        return t

def t_ZERO(t):
    r'(?:BIG|GREAT)[ ]O[ ]FRUM[ ]THE[ ]SKY\b'
    return NUMBER(t, '0')

def t_HELLO_WORLD(t):
    r'SAY[ ]H[I]+\b'
    return INLINE(t, 'print "HAY WERLD!!11!"')

def t_SIX(t):
    r'UPSIDE[ ]DOWN[ ]NINE'
    return NUMBER(t, '6')

def t_EIGHT(t):
    r'SIDEWAYS[ ]INFINTY'
    return NUMBER(t, '8')

def t_NINE(t):
    r'UPSIDE[ ]DOWN[ ]SIX'
    return NUMBER(t, '9')

def t_NAME(t):
    r'[a-zA-Z_]\w*'
    if t.value in RESERVED_WORDS:
        t.type, t.value = RESERVED_WORDS[t.value]
        if t.type == 'AUTOCALL':
            t.lexer.paren_stack.append(')')
    return t

# The following contains all reserved words currently added to the language.
# If any one is used in the LOLCODE, the t_NAME token match will swap it out
# for the appropriate type and Python value, as defined in the dictionary.

RESERVED_WORDS = {
    'TXT': ('AUTOCALL', 'str'),
    'NUMBR': ('AUTOCALL', 'int'),
    'NUMBRZ': ('AUTOCALL', 'range'),
    'BIGNUMBRZ': ('AUTOCALL', 'xrange'),
    'ADD': ('AUTOCALL', '.append'),
    
    'ANTI': ('INLINE', '-'),
    '-': ('INLINE', '-'),
    'MY': ('INLINE', 'self.'),
    'ME': ('INLINE', '(self)'),
    'NUFFING': ('INLINE', "''"),
    'RANDUMB': ('INLINE', 'random'),
    'STRT': ('INLINE', '__init__'),
    'THING': ('INLINE', '()'),
    'UP': ('INLINE', '+= 1'),

    'EVILTWINZ': ('NUMBER', '-2'),
    'ZERO': ('NUMBER', '0'),
    'CHEEZBURGER': ('NUMBER', '1'),
    'UNO': ('NUMBER', '1'),
    'TWINS': ('NUMBER', '2'),
    'TWINZ': ('NUMBER', '2'),
    'TOO': ('NUMBER', '2'),
    'TREE': ('NUMBER', '3'),
    'FER': ('NUMBER', '4'),
    'FIVER': ('NUMBER', '5'),
    'SEVEN': ('NUMBER', '7'),
    'NINER': ('NUMBER', '9'),
    'ALLTOEZ': ('NUMBER', '10'),
    'ALLFINGERZ': ('NUMBER', '10'),
    'TEN': ('NUMBER', '10'),
    'LEVEN': ('NUMBER', '11'),
    'DUZN': ('NUMBER', '12'),
    'BAKRDUZN': ('NUMBER', '13'),
    'FERTEEN': ('NUMBER', '14'),
    'SCHFIFTEEN': ('NUMBER', '15'),
    'SWEETSIXTEEN': ('NUMBER', '16'),
    'SEVENTEEN': ('NUMBER', '17'),
    'EIGHTEEN': ('NUMBER', '18'),
    'NINETEEN': ('NUMBER', '19'),
    'TWENTY': ('NUMBER', '20'),
    'THIRTY': ('NUMBER', '30'),
    'FORTY': ('NUMBER', '40'),
    'THEANSWERTOLIFETHEUNIVERSEANDEVERYTHINGLOL': ('NUMBER', '42'), 
    'FIFTY': ('NUMBER', '50'),
    'SCHFIFTYFIVE': ('NUMBER', '55'),
    'SIXTY': ('NUMBER', '60'),
    'SEVENTY': ('NUMBER', '70'),
    '8EE': ('NUMBER', '80'),
    'NINETY': ('NUMBER', '90'),
    'ACREWOOD': ('NUMBER', '100'),
    'HUNNID': ('NUMBER', '100'),

    'AND': ('OP', ','),
    'OVER': ('OP', '/'),
    'OWN': ('OP', '.'),
    
    'GIMME': ('RESERVED', 'import'),
    'IZ': ('RESERVED', 'if'),
    'LIEK': ('RESERVED', 'as'),
    'LOLSYS': ('RESERVED', 'sys'),
    'WHILE': ('RESERVED', 'while'),
    'STOPSTOPSTOP': ('RESERVED', 'break'),
    'KGO': ('RESERVED', 'continue'),
    'PLZ': ('RESERVED', 'try'),
    'WIF': ('RESERVED', 'with'),
    'BLACKHOLE': ('RESERVED', 'ZeroDivisionError'),
    'DONOTWANT': ('RESERVED', 'AssertionError'),
    'SOWRONGSOWRONGSOWRONG': ('RESERVED', 'ValueError')
}

def convert_to_python(s):
    """Converts a string of LOLPython code to normal Python code."""
    body = StringIO.StringIO()
    lexer = lex.lex()
    lexer.paren_stack = []
    lexer.input(s)

    for t in iter(lexer):
        if t.type == 'NAME':
            body.write(t.value + (t.value in keyword.kwlist and '_ ' or ' '))
        elif t.type == 'AUTOCALL':
            body.write(t.value + '(')
        elif t.type in ['RESERVED', 'CLOSEBLOCK']:
            body.write(t.value + ' ')
        elif t.type == 'STRING':
            body.write(repr(t.value) + ' ')
        elif t.type == 'DOCSTRING':
            body.write('"""%s"""' % t.value)
        elif t.type == 'COMMENT':
            body.write('#' + t.value[3:])
        elif t.type in ['NEWLINE', 'WS', 'INLINE', 'NUMBER', 'OP']:
            body.write(t.value)
        elif t.type == 'PRINT':
            if t.value == 'stdout':
                body.write('print')
            elif t.value == 'stderr':
                body.write('print >>sys.stderr, ')
            else:
                raise AssertionError(t.value)

    return body.getvalue()

def execute_file(inc):
    """Executes the given LOLPython file and returns the python equivalent."""
    if hasattr(inc, 'read'):
        return execute_string(inc.read())
    return execute_string(open(inc, 'r').read())

def execute_string(s, module_name='__lolmain__'):
    """Executes a string of LOLPython code and returns the python equivalent."""
    std_python_str = convert_to_python(s)
    exec std_python_str
    return std_python_str

def convert_file(filename_in):
    """Converts the given LOLPython file to a similarly named .py file,
    translated into python."""
    fin = open(filename_in, 'r')
    filename_out = '.lol' in filename_in and \
                   filename_in.replace('.lol', '.py') or (filename_in + '.py')
    fout = open(filename_out, 'w')
    fout.write(convert_to_python(fin.read()))
    fout.close()
    fin.close()

HELP_TEXT = '''Runs or converts a LOLPython program.

    lolpython             Reads and executes a LOLPython program from stdin
    lolpython filename    Reads and executes the LOLPython code in filename'''

def main():
    parser = optparse.OptionParser(usage=HELP_TEXT)
    parser.add_option('-c', '--convert', metavar='filename',
                      help='Converts LOLPython code from filename. ' +
                           'Outputs to filename.py')
    parser.add_option('-v', '--version', action='store_true',
                      dest='disp_version',
                      help='Displays the version number.')
    options, args = parser.parse_args()

    if options.disp_version:
        print 'LOLPython, version %.1f' % __version__
    elif options.convert:
        convert_file(options.convert)
    elif len(args) != 1:
        print HELP_TEXT
    else:
        execute_file(args[0])

if __name__ == '__main__':
    main()

