""" js2esi.token
ply.lex token definitions for js-to-esi compilation
"""

import re

import sys

__author__ = "<NAME>, <NAME>"
__copyright__ = "Copyright 2017, Akamai Technologies"
__license__ = "Apache2"


class UnicodeNotSupported(Exception): pass


class BadEscapeSequence(Exception): pass


# reserved words
reserved = (
    # module keywords
    'require',
    'inline',
    # function keywords
    'function',
    'return',
    # conditional keywords
    'if',
    'else',
    # looping keywords
    'for',
    'of',
    #'in',
    'break',
    # exception handling keywords
    'try',
    'except',
    'catch',
    # pre-defined special functions
    'eval',
    'include',
    # regex operators
    'has',
    'has_i',
    'matches',
    'matches_i',
    'as',
    # constants
    'false',
    'true',
    # variable definitions
    'let',
    'var',
    'const',
    # compatibility properties
    # 'length',
    # 'indexOf',
    # 'charAt',
)

# esi pre-defined functions
functions = (
    # 'string_split',
    # 'join',
    # 'index',
    # 'rindex',
    # 'lstrip',
    # 'rstrip',
    # 'strip',
    # 'replace',
    # 'substr',
    # 'lower',
    # 'upper',
    # 'convert_to_unicode',
    # 'convert_from_unicode',
    # 'str',
    # 'int',
    # 'bin_int',
    # 'len',
    # 'list_delitem',
    # 'rand',
    # 'last_rand',
    # 'is_empty',
    # 'exists',
    # 'add_header',
    # 'set_response_code',
    # 'set_redirect',
    # 'add_cachebusting_header',
    # 'url_encode',
    # 'url_decode',
    # 'html_encode',
    # 'html_decode',
    # 'base64_encode',
    # 'base64_decode',
    # 'digest_md5',
    # 'digest_md5_hex',
    # 'time',
    # 'http_time',
    # 'strftime',
    # # tbd: these should probably only ever be generated by js, not
    # #      the js'er...
    # 'dollar',
    # 'dquote',
    # 'squote',
)

# list of token names
tokens = (
             'NUMBER',
             'STRING',
             'LPAREN',
             'RPAREN',
             'LBRACE',
             'RBRACE',
             'LBRACKET',
             'RBRACKET',
             'COMMA',
             'COLON',
             'ASSIGN',
             'ASSIGNPLUS',
             'ASSIGNMINUS',
             'ASSIGNMULTIPLY',
             'ASSIGNMODULUS',
             'ASSIGNDIVIDE',
             'INCREMENT',
             'DECREMENT',
             'STOP',
             'EQUAL',
             'NOTEQUAL',
             'LESSERTHAN',
             'LESSEROREQUAL',
             'GREATERTHAN',
             'GREATEROREQUAL',
             'PLUS',
             'MINUS',
             'MULTIPLY',
             'MODULUS',
             'DIVIDE',
             'RANGE',
             'DOT',
             'NOT',
             'AND',
             'OR',
             'BITWISENOT',
             'BITWISEAND',
             'BITWISEOR',
             'BITWISEXOR',
             'SHIFTLEFT',
             'SHIFTRIGHT',
             'QUESTION',
             'ESICOMMENT',
             'ESICOMMENT_CONT',
             'SYMBOL',
         ) \
         + tuple([e.upper() for e in reserved]) \
         + tuple([e.upper() for e in functions])

# regular expression rules for simple tokens
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_LBRACE = r'\{'
t_RBRACE = r'\}'
t_LBRACKET = r'\['
t_RBRACKET = r'\]'
t_COMMA = ','
t_COLON = ':'
t_ASSIGN = '='
t_ASSIGNPLUS = r'\+='
t_ASSIGNMINUS = '-='
t_ASSIGNMULTIPLY = r'\*='
t_ASSIGNMODULUS = '%='
t_ASSIGNDIVIDE = '/='
t_INCREMENT = r'\+\+'
t_DECREMENT = '--'
t_STOP = ';'

# operators
t_EQUAL = r'===?'
t_NOTEQUAL = r'!==?'
t_LESSERTHAN = '<'
t_LESSEROREQUAL = '<='
t_GREATERTHAN = '>'
t_GREATEROREQUAL = '>='
t_PLUS = r'\+'
t_MINUS = '-'
t_MULTIPLY = r'\*'
t_MODULUS = '%'
t_DIVIDE = '/'
t_RANGE = r'\.\.'
t_DOT = r'\.'
t_NOT = '!'
t_AND = '&&'
t_OR = r'\|\|'
t_BITWISENOT = '~'
t_BITWISEAND = '&'
t_BITWISEOR = r'\|'
t_BITWISEXOR = r'\^'
t_SHIFTLEFT = '<<'
t_SHIFTRIGHT = '>>'
t_QUESTION = r'\?'
#t_QUOTE = r'[\'"]'


# TODO: TODO
#   <<, >>
#   ~, &, |, ^
#   .. (range)

# literals: numbers, strings, etc...
def t_NUMBER(t):
    r'\d+|0x\d+'
    t.value = int(t.value)
    return t


escapeMap = {
    'n': '\n',
    'r': '\r',
    't': '\t',
}


def slashHandler(m):
    esc = m.group(1)
    if esc in escapeMap:
        return escapeMap[esc]
    if esc[0] == 'u' or esc[0] == 'U':
        raise UnicodeNotSupported(esc)
    if esc[0] == 'x':
        return chr(int(esc[1:], 16))
    return esc


def t_STRING(t):
    r'("([^"\\]|\\[^xuU]|\\x[\da-fA-F]{2}|\\u[\da-fA-F]{4}|\\U[\da-fA-F]{8})*"|\'([^\'\\]|\\[^xuU]|\\x[\da-fA-F]{2}|\\u[\da-fA-F]{4}|\\U[\da-fA-F]{8})*\')'
    t.lexer.lineno += len(t.value.split('\n')) - 1
    t.value = re.sub(r'\\([^xuU]|x[\da-fA-F]{2}|u[\da-fA-F]{4}|U[\da-fA-F]{8})', slashHandler, t.value[1:-1])
    return t


# define a rule so we can track line numbers
def t_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)


# ESI comments (ie. comments that should stay in the output)
def t_ESICOMMENT(t):
    r'//@esi-comment.*'
    t.value = t.value[15:]
    t.lexer.push_state('multicomment')
    t.lexer.mclast = t.lexer.lexpos
    return t

# symbols
def t_SYMBOL(t):
    r'[a-zA-Z_][a-zA-Z_0-9]*'
    # check for reserved words
    if t.value in reserved or t.value in functions:
        t.type = t.value.upper()
    return t


# ignore handling rules

t_ignore = ' \t'
t_ignore_JSCOMMENT = r'\/\/.*'
t_ignore_JSLONGCOMMENT = r'\/\*(.|\n)*?\*\/' #TODO: track and output?

# error handling rules
def t_error(t):
    t.lexer.error('illegal lexical character "%s" (in %s context)' % (t.value[0], t.lexer.lexstate))
    t.lexer.skip(1)


# special state: 'multicomment' (continuation of multi-line esi comments)

def t_multicomment_ESICOMMENT_CONT(t):
    r'//@esi-comment.*'
    t.value = t.value[15:]
    t.lexer.mclast = t.lexer.lexpos
    return t


def t_multicomment_OTHER(t):
    r'.+'
    t.lexer.pop_state()
    t.lexer.lineno -= len(t.lexer.lexdata[t.lexer.mclast:t.lexer.lexpos].split('\n')) - 1
    t.lexer.skip(t.lexer.mclast - t.lexer.lexpos)


def t_multicomment_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)


t_multicomment_ignore = ' \t'
t_multicomment_error = t_error

# states
states = (
    ('multicomment', 'exclusive'),
)

