#!python
import dateutil
import re
import code
from datetime import datetime, date, timedelta
import time
from ply.lex import TOKEN

replace = lambda replacee,replacer,string: re.sub(replacee, replacer, string)

tokens = (
    'PLUS','MINUS','EQUALS',
    'LPAREN','RPAREN',
    'TIME_INVALID',
    'TIME_MS',
    'TIME_HM',
    'TIME_HMS',
    'N', # now
    'T', # today
    'YD', # yesterday
    'DELTA', 
    'DATE', 
    'PERIOD', 
    'SEMICOLON', 
    # 'DATETIME', 
    'NAME',
    )

# Tokens

t_SEMICOLON = r';'
t_PERIOD    = r'\.'
t_PLUS      = r'\+'
t_MINUS     = r'-'
t_EQUALS    = r'='
t_LPAREN    = r'\('
t_RPAREN    = r'\)'

t_NAME      = r'[a-zA-Z_][a-zA-Z0-9_]*'

def t_YD(t):
    r'[yY][dD]'
    t.value = datetime.today() - timedelta(days=1)
    return t

def t_T(t):
    r'[tT]'
    t.value = datetime.today()
    return t

def t_N(t):
    r'[nN]'
    t.value = datetime.now()
    return t

def t_TIME_HMS(t):
    r'(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])'
    t.value = datetime.strptime(t.value, '%H:%M:%S')
    return t

def t_TIME_INVALID(t):
    r'\d+:\d+'
    raise SyntaxError('Ambiguous time definition')

def t_TIME_MS(t):
    r'(2[0-3]|[01][0-9])[M]:(60|[0-5][0-9])[sS]'
    t.value = datetime.strptime(replace('[Ms]', '', t.value), '%M:%S')
    return t

def t_TIME_HM(t):
    r'(2[0-3]|[01][0-9])[hH]:([0-5][0-9])[M]'
    t.value = datetime.strptime(replace('[hHM]', '', t.value), '%H:%M')
    return t

def t_DATE(t):
    r'\d+(\D)(1[0-2]|0[1-9])\2(3[01]|[12][0-9]|[1-9]) '
    t.value = datetime.strptime(replace(r'\D', '-', t), '%Y-%M-%D')
    return t

DELTA_TOKEN = r'((\d+(?:\.\d+)?)([sSMhHdDmyY]))+'
@TOKEN(DELTA_TOKEN)
def t_DELTA(t):
    units_vals = {}
    for v,u in re.findall(DELTA_TOKEN[1:-2], t.value):
        units_vals.update({ u if u in 'mM' else u.lower(): float(v) if v else 1 })
    t.value = timedelta()
    if 's' in units_vals:
        t.value += timedelta(seconds=units_vals['s'])
    if 'M' in units_vals:
        t.value += timedelta(minutes=units_vals['M'])
    if 'h' in units_vals:
        t.value += timedelta(hours=units_vals['h'])
    if 'd' in units_vals:
        t.value += timedelta(days=units_vals['d'])
    if 'm' in units_vals:
        t.value += timedelta(months=units_vals['m'])
    if 'y' in units_vals:
        t.value += timedelta(months=units_vals['y']*12)
    return t

def t_DATETIME(t):
    r'\d+(\D)(1[0-2]|0[1-9])\2(3[01]|[12][0-9]|[1-9]) ' 
    t.value = datetime.strptime(replace(r'\D', '-', t), '%Y-%M-%D')
    return t

t_ignore = ' \t'
t_ignore_COMMENT = r'\#.*'

def t_newline(t):
    r'\n+'
    t.lexer.lineno += t.value.count('\n')

def t_error(t):
    print(f'Illegal character {t.value[0]!r}')
    t.lexer.skip(1)

import ply.lex as lex
lex.lex(debug=0)

precedence = (
    ('left',
        'PLUS',
        'MINUS', 

        ),
    ('right',
        'UMINUS',

        ),
    )


days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
days_abbrev = [d[:3] for d in days]

def wait(t):
    if isinstance(t, datetime):
        now = datetime.now()
        delta = t - now
    elif isinstance(t, timedelta):
        delta = t
    else:
        raise SyntaxError('wait accepts datetime or timedelta only')
    if delta > timedelta(0):
        time.sleep(delta.total_seconds())

names = {
            'dow'    : lambda t: days[t.weekday()],
            'day'    : lambda t: t.day,
            'month'  : lambda t: t.month,
            'year'   : lambda t: t.year,
            'hour'   : lambda t: t.hour,
            'minute' : lambda t: t.minute,
            'second' : lambda t: t.second,

            'wait'   : lambda t: wait(t),
            'next'   : lambda t: next_wd(t)
        }

def p_statements(p):
    'statement : statement SEMICOLON statement'
    pass

def p_statement_assign(p):
    'statement : NAME EQUALS expression'
    names[p[1]] = p[3]

def p_statement_expr(p):
    'statement : expression'
    if p[1]:
        if isinstance(p[1], datetime) and \
                p[1].year == 1900:
                    print(datetime.strftime(p[1], '%H:%M:%S'))
        else:
            print(p[1])
        names['_'] = p[1]

def p_expression_binop(p):
    '''expression : expression PLUS expression
                  | expression MINUS expression'''
    if isinstance(p[1], datetime) and \
            isinstance(p[3], int):
        raise SyntaxError('missing time unit')
    if p[1] is None or p[3] is None:
        raise SyntaxError(f'in {p[2]} expr, p[1]={p[1]} and p[3]={p[3]}')
    if   p[2] == '+': 
        if isinstance(p[1], datetime) and \
                isinstance(p[3], datetime):
            raise SyntaxError('can\'t add time points')
        p[0] = p[1] + p[3]
    elif p[2] == '-': 
        p[0] = p[1] - p[3]

def p_expression_datetime(p):
    '''expression : TIME_MS 
                  | TIME_HM
                  | TIME_HMS
                  | DATE
                  | YD
                  | N
                  | DELTA
                  | T
                  '''
    p[0] = p[1]

def p_expression_name(p):
    'expression : NAME'
    try:
        p[0] = names[p[1]]
    except LookupError:
        try:
            p[0] = names[p[3]](p[1])
        except LookupError:
            print("Undefined name '%s'" % p[1])
            p[0] = 0

def p_expression_uminus(p):
    'expression : MINUS expression %prec UMINUS'
    p[0] = -p[2]

def p_expression_get_attribute(p):
    'expression : expression PERIOD NAME'
    if p[3] is None:
        raise SyntaxError
    p[0] = names[p[3]](p[1])

def p_expression_funcall(p):
    'expression : NAME LPAREN expression RPAREN'
    p[0] = names[p[1].lower()](p[3])

def p_expression_invalid_time(p):
    'expression : TIME_INVALID'

def p_expression_group(p):
    'expression : LPAREN expression RPAREN'
    p[0] = p[2]

import ply.yacc as yacc
yacc.yacc()

if __name__ == '__main__':
    import cmd
    class CmdParse(cmd.Cmd):
        prompt = ''
        commands = []
        def default(self, line):
            if line == 'EOF':
                exit(0)
            yacc.parse(line)
            self.commands.append(line)
        def do_help(self, line):
            print('''
OBJECTS:

    DELTA:  

            a timedelta object can be interpreted as 
            sequence consisting of a number followed 
            by a time unit in ISO format, although
            it's case is relaxed where there is no
            case differentiation:
CLARIFY ----^^^^^^^^^^^^^^^^^^^^

                1D
                -2M2s
                10Y33s

    POINT:

            a datetime object that represents a
            point in time. Can be interpreted 
            in various forms such as follows:

                2020/12/31
                22h:22M
                2020/12/31 22:22
                2020/12/31 22:22:22

    POINT ATTRIBUTES:
            
            a point in time has specific parts that
            can be extracted as such:

                2020/12/31.dow # as in, day of week

VARIABLES:

            There are three built-in variables:

            T  - today
            YD - yesterday, based on today
            N  - now, which approximates T but also 
                 includes timezone info

            But you can also assign objects to a 
            named variable, like so:
                foo=1d
                bar=YD


FUNCTIONS: 

    wait(DELTA)   - sleeps for that timedelta
    next(WEEKDAY) - returns the date for the next weekday
                    ''')
        def do_exit(self, line):
            return True
    CmdParse().cmdloop()
