#!/usr/bin/env python
from collections import OrderedDict
from datetime import datetime, date, timedelta, time
import dateutil
from dateutil.relativedelta import relativedelta,MO,TU,WE,TH,FR,SA,SU
from ply.lex import TOKEN
from time import sleep, mktime
from types import SimpleNamespace as Namespace
import appdirs
import calendar
import code
import json
import locale
import os
import os.path as op
import re
import sys
import types

def read_write_get_config():
    config_dir = appdirs.user_config_dir()
    dte_config_dir = config_dir + '/dte'
    dte_config_file = dte_config_dir + '/config.json'
    default_config = { 
            'timestamp_unit': 'seconds',
            'clock': '24',
            'datetime_output_format': 'ISO8601' 
            }
    if not op.exists(dte_config_dir):
        os.mkdir(dte_config_dir)

    if op.exists(dte_config_file):
        try:
            config_dict = json.load(open(dte_config_file))
            updated = False
            for k,v in default_config.items():
                if not k in config_dict:
                    config_dict.update({k:v})
                    updated = True
            if updated:
                json.dump(config_dict, open(dte_config_file, 'w'), indent=4)
        except Exception as e:
            print('Configuration file is malformed.\n'+\
                    'Falling back to default configuration', file=sys.stderr)
            config_dict = default_config
    else:
        json.dump(default_config, open(dte_config_file, 'w'), indent=4)
        config_dict = default_config
    return config_dict

config = Namespace(**read_write_get_config())

HELP = '''
SYNTAX


OBJECTS

    DELTA  

            a timedelta object can be interpreted as 
            chain of (amount, unit) consisting of a 
            number followed by a time unit in ISO format, 
            with case input relaxed except for 
            differentiating months and minutes:

                1D+1d  # case insensitive
                3M+3m  # except for months and minutes
               -2m+2s  # accepts negative numbers
                10Y3s  # join them together instead of adding
                10h30  # omit the last unit and dte will
                       # interpret it as the immediatelly
                       # smaller unit

    DATETIME

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

                1611269086 # unix timestamp in seconds
                2020/12/31 22:22
                2020 Jan 12
                2020 December 20
                2020/12/31 22:22:22
                2020/12/31
                today

            it can also represent time only:

                22:22:22
                22h:22
                22H:22
                22m:22
                22:22s
                22:22m
                6 pm
                6h:20 am

            notice that the combination of NUMBER:NUMBER
            is ambiguous and therefore invalid syntax.

VARIABLES

            there are three built-in variables:

                T or today     
                YD or yesterday
                TM or tomorrow 
                N  or now      

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

OPERATORS
            +  : adds deltas to points in time
            -  : takes de difference between two points 
                 in time and stores a delta

 < <= > == !=  : compares two points in time and returns
                 a boolean

KEYWORDS
            in   : the `in` keyword has two purposes,
                   interpreting a DELTA object and a unit
                   and converting the former into the latter
                   such as:

                   1d3h in seconds

                   it can also convert a point in time to a 
                   unix timestamp, which can be configured in
                   seconds or milliseconds using the config
                   file:

                   1970 Jan 1 in unix

            until: takes a point in time and a unit and shows
                   the the amount of the unit until the time
                   point:

                   seconds until 3000 Apr 25

FUNCTIONS / ATTRIBUTES 
              
    wait DELTA       : sleeps for the duration

    next WEEKDAY     : returns the date for the next weekday

    lastWEEKDAY     : returns the date for the last weekday

dayofweek TIME_POINT : returns weekday for time point
    dow  TIME_POINT  : ⏎
    TIME_POINT.dow   : ⏎

STDIN

       The  standard  input  shall be used only if no arguments are specified.

'''

n = None

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

relativedelta_days = [MO,TU,WE,TH,FR,SA,SU]

days = list(calendar.day_name)
months = list(calendar.month_name)[1:]
days_abbrev = list(calendar.day_abbr)
months_abbrev = list(calendar.month_abbr)[1:]

days_en = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
days_en_abbrev = [d[:3] for d in days_en]
days_en_abbrev_max = [d[:2] for d in days_en]
months_en = ['January', 'February', 'March', 'April', 'May', 'June', \
            'July', 'August', 'September', 'October', 'November', 'December']
months_en_abbrev = [m[:3] for m in months_en]

tokens = (
    'PLUS','MINUS','EQUALS',
    'LPAREN','RPAREN',
    # 'YD',
    # 'N',
    # 'T',
    # 'TM',
    'UNIT',
    'CYCLIC_OP',
    'IN',
    'WAIT',
    'UNTIL',
    'SINCE',
    'GT', 'GE', 'LT', 'LE', 'EQ', 'NE',
    'NAME',
    'INTEGER',
    'YEAR',
    'MONTH',
    'BASEDATE',
    'DELTA', 
    'WEEKDAY', 
    'PERIOD', 
    'SEMICOLON', 
    'COLON', 
    'DATETIME', 
    )

# Tokens

t_COLON     = r':'
t_SEMICOLON = r';'
t_PERIOD    = r'\.'
t_PLUS      = r'\+'
t_MINUS     = r'-'
t_EQUALS    = r'='
t_LPAREN    = r'\('
t_RPAREN    = r'\)'
t_GT        = r'>'
t_GE        = r'>='
t_LT        = r'<'
t_LE        = r'<='
t_EQ        = r'=='
t_NE        = r'!='
t_IN        = r'(?i)in'
t_UNTIL     = r'(?i)until'
t_SINCE     = r'(?i)since'
t_WAIT      = r'(?i)wait'

reserved = [
            '|'.join(days),
            '|'.join(days_abbrev),
            '|'.join(months),
            '|'.join(months_abbrev),
            '|'.join(days_en),
            '|'.join(days_en_abbrev),
            # '|'.join(days_en_abbrev_max),
            '|'.join(months_en_abbrev),
            r'in',
            # r'n(ow)?',
            r'next|last',
            r'seconds|minutes|hours|days|weeks',
            # r't(oday)?',
            r'until',
            r'since',
            # r'wait',
            # r'yd|yesterday',
            # r'tm|tomorrow',
        ]

def is_reserved(k):
    for r in reserved:
        if re.match('(?i)'+r, k):
            return True

REGEX_ALL_MONTHS = r'|'.join(months) + r'|' + \
                   r'|'.join(months_abbrev) + r'|' + \
                   r'|'.join(months_en_abbrev)
REGEX_DOY = r'(\d+(?!:)(?:\W)\d+(?!:)(?:\W)\d+|\d+[\W\s]+(?:' + \
        REGEX_ALL_MONTHS + \
        r')[\W\s]+\d+)'

REGEX_0_23 = r'(2[0-3]|1?[0-9]|00)'
REGEX_0_59 = r'([0-5]?[0-9])'

# https://regexr.com/5lclj
DATETIME_REGEX = \
    f'(?i)(?:{REGEX_DOY}\s?|' + \
        f'(?:(\d)\s+([aA]|[pP][mM])|' + \
            f'{REGEX_0_59}([hHM])?' + \
                f'(?::{REGEX_0_59}([MsS])?' + \
                    f'(?::(?:{REGEX_0_59}([sS])?))?' + \
                 ')'+ \
         ')(?:\s+([aA]|[pP])[mM])?' + \
     ')' + '{1,2}'

# print(DATETIME_REGEX)
@TOKEN(DATETIME_REGEX)
def t_DATETIME(t):
    date_str,\
    zeroth_val,\
    zeroth_ampm,\
    first_val,\
    first_unit,\
    second_val,\
    second_unit,\
    third_val,\
    third_unit,\
        ampm = re.search(DATETIME_REGEX, t.value).groups()

    if zeroth_val and zeroth_ampm:
        t.value = datetime.strptime(f'{zeroth_val} {zeroth_ampm}', '%I %p').time()
        return t

    if t.value.count(':') == 1 and \
            not first_unit and not second_unit \
                and not third_unit and not third_val:
                    raise Exception(f'Did you mean {first_val}h:{second_val} or ' + \
                                                  '{first_val}m:{second_val}?')

    if ampm is not None and \
            (first_unit == 'h' or \
                first_unit == 'H' or \
                second_unit == 'm' or \
                third_val) and \
                int(first_val ) > 12:
                    raise Exception('Conflicting 24-hour time in 12-hour clock')

    if date_str:
        if any(month.lower() in date_str.lower() for month in months_abbrev + months):
            y,b,d = replace(r'[\W\s]+', ' ', date_str).split(' ')
            try:
                date = datetime.strptime(f'{y.zfill(4)}-{b}-{d}', '%Y-%b-%d')
            except:
                try:
                    date = datetime.strptime(f'{y.zfill(4)}-{b}-{d}', '%Y-%B-%d')
                except:
                    raise Exception(f'Invalid syntax: {date_str}')
        else:
            y,M,d = replace(r'\D', '-', date_str).split('-')
            date = datetime.strptime(f'{y.zfill(4)}-{M}-{d}', '%Y-%m-%d').date()
        if (first_val,second_val,third_val) == (None,None,None):
            t.value = date
            return t
    else:
        date = datetime.today()
    is_HMS = t.value.count(':') == 2
    is_HM = 'h' == first_unit or \
            'H' == first_unit or \
            'm' == second_unit or \
            (third_unit is not None and \
                ('s' == third_unit or 'S' == third_unit))
    if ampm is not None and \
            (first_unit == 'h' or first_unit == 'H' or \
                second_unit == 'm'):
                    if first_val != '12':
                        if ampm.lower() == 'p':
                            first_val = f'{(int(first_val)+12)}' 
                    else:
                        if ampm.lower() == 'a':
                            first_val = 0

    H_or_M,M_or_S,S = (first_val,second_val,third_val)
    if is_HMS:
        le_time = datetime.strptime(f'{H_or_M}:{M_or_S}:{S}', '%H:%M:%S').time()
    elif is_HM:
        le_time = datetime.strptime(f'{H_or_M}:{M_or_S}', '%H:%M').time()
    else:
        le_time = datetime.strptime(f'{H_or_M}:{M_or_S}', '%M:%S').time()
    t.value = le_time if not date_str else datetime.combine(date,le_time)
    return t

unit_map = {
        's': 'seconds',
        'S': 'seconds',
        'm': 'minutes',
        'h': 'hours',
        'H': 'hours',
        'd': 'days',
        'D': 'days',
        'w': 'weeks',
        'W': 'weeks',
        'M': 'months',
        'y': 'years',
        'Y': 'years',
        }

UNITS_STR = ''.join(unit_map.keys())
FLOATING_POINT = r'((?:\d*[.])?\d+)'
DELTA_TOKEN = FLOATING_POINT + '['+UNITS_STR+']('+FLOATING_POINT+'(['+UNITS_STR+']?))*'
@TOKEN(DELTA_TOKEN)
def t_DELTA(t):
    units_vals = OrderedDict()
    matches = re.findall(FLOATING_POINT + '(['+UNITS_STR+']|$)', t.value)
    for v,u in matches:
        units_vals.update({ u if u in 'mM' else u.lower(): float(v) if v else 1 })
    t.value = parse_units(units_vals)
    if '' in units_vals:
        u,_ = list(units_vals.items())[list(units_vals.keys()).index('')-1]
        next_unit = UNITS_STR[UNITS_STR.index(u)-1]
        if next_unit.lower() == u.lower():
            next_unit = UNITS_STR[UNITS_STR.index(u)-2]
        le_add = parse_units({next_unit: units_vals['']}) # get successor delta
        t.value += le_add
    return t

REGEX_1_12 = '(1[0-2]|[1-9])'
BASEDATE_REGEX = r'(?i)(?:' + \
        r'('+REGEX_ALL_MONTHS+r')(?!:|\+)\W(\d+)|' + \
        r'(\d+)(?!:|\+)\W('+REGEX_ALL_MONTHS+')|' + \
        r'(\d+)(?!:|\+)\W'+REGEX_1_12 + \
        ')'

def get_month_index_by_name(month_name):
    for month_collection in (months, months_abbrev, months_en_abbrev):
        if month_name in month_collection:
            return month_collection.index(month_name) + 1

# print(BASEDATE_REGEX)
@TOKEN(BASEDATE_REGEX)
def t_BASEDATE(t):
    m1,y1,y2,m2,y3,m3 = re.search(BASEDATE_REGEX, t.value).groups()
    if m1:
        month = m1
        year = y1
    if m2:
        month = m2
        year = y2
    if m3:
        month = m3
        year = y3
    year = int(year)
    month_index = get_month_index_by_name(month)
    if not month_index:
        month_index = int(month)
    t.value = datetime(year, month_index, 1)
    return t

t_NAME = '(?!' + '|'.join(reserved) + ')([a-zA-Z_][a-zA-Z0-9_]*)'

@TOKEN(r'(?i)' + '|'.join(days) + '|' + '|'.join(days_abbrev) + '|' + '|'.join(days_en_abbrev))
def t_WEEKDAY(t):
    t.value = t.value
    return t

@TOKEN(r'(?i)' + '|'.join(months) + '|'.join(months_abbrev))
def t_MONTH(t):
    t.value = Month(t.value)
    return t

def t_CYCLIC_OP(t):
    r'(?i)(next|last|first)'
    return t

def t_UNIT(t):
    r'(?i)(seconds|minutes|hours|days|weeks|months|years|unix)'
    return t

def get_closest_week_day(week_day):
    counter_next = 0
    counter_prev = 0
    next_date = datetime.now()
    for _ in range(7):
        next_date += timedelta(days=1)
        counter_next += 1
        if days[next_date.weekday()].lower() == week_day.lower():
            break
    prev_date = datetime.now()
    for _ in range(7):
        prev_date += timedelta(days=-1)
        counter_prev += 1
        if days[prev_date.weekday()].lower() == week_day.lower():
            break
    if counter_next < counter_prev:
        return next_date.date()
    return prev_date.date()

def parse_units(units_vals):
    parsed = timedelta()
    for unit,long_name in unit_map.items():
        if unit in units_vals:
            if unit.lower() == 'y':
                parsed += (datetime.now()+relativedelta(years=units_vals[unit])) - datetime.now()
            elif unit == 'M':
                parsed += (datetime.now()+relativedelta(months=units_vals[unit])) - datetime.now()
            else:
                parsed += timedelta(**{long_name: units_vals[unit]})
    return parsed

def t_INTEGER(t):
    r'\d+'
    t.value = int(t.value)
    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}',file=sys.stderr)
    t.lexer.skip(1)

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

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

def dow(t):
    if type(t) == date or type(t) == datetime:
        return days[t.weekday()]
    elif type(t) == timedelta:
        return days[(datetime.now()+t).weekday()]
    else:
        raise Exception('Can\'t get day of week of object of type' + str(type(t)))

def is_000(obj):
    return obj.hour == obj.minute == obj.second == 0 if type(obj) == datetime \
            else type(obj) == date 

class Weekday:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

class Month:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

names = {
            '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),
            'dow'       : lambda t                           : dow(t),
            'dow'       : lambda t                           : dow(t),
            'dayofweek' : lambda t                           : dow(t),
            'weekday'   : lambda t                           : dow(t),
            'help'      : lambda                             : print(HELP),
        }

precedence = (
    ('right',
        'UMINUS',
        ),
    ('left',
        'UNIT',
        'PLUS',
        'MINUS', 
        'NAME',
        ),
    )

def p_statement_wait_until_point(p):
    'statement : WAIT UNTIL point'
    wait(p[3])

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

def p_statement_invalid_assignment(p):
    '''statement : WEEKDAY EQUALS expression
                 '''
                 # | YD EQUALS expression
                 # | TM EQUALS expression
                 # | T EQUALS expression
                 # | N EQUALS expression
    raise Exception(f'Can\'t assign expression to {p[1]} keyword')

def p_statement_assign(p):
    'statement : NAME EQUALS expression'
    global n
    n = None
    if is_reserved(p[1]):
        raise Exception('Can\'t use reserved keyword')
    names[p[1]] = p[3]

def p_expression_name(p):
    'expression : NAME'
    if p[1] not in names:
        print(f'{p[1]} is not loaded')
        return
    if callable(names[p[1]]):
        p[0] = normalize(names[p[1]]())
    else:
        p[0] = names[p[1]]

def normalize(t):
    if type(t) == datetime and \
            is_000(t):
        t = t.date()
    return t

def p_statement_expr(p):
    'statement : expression'
    if type(p[1]) is Weekday:
        p[1] = get_closest_week_day(str(p[1]))
    if type(p[1]) == dateutil._common.weekday:
        p[1] = (datetime.now() - relativedelta(weekday=p[1])).date()
    if type(p[1]) == relativedelta:
        p[1] = (datetime.now() + p[1]).date()
    if p[1] is not None:
        if config.datetime_output_format != 'ISO8601':
            print(normalize(p[1]).strftime(config.datetime_output_format))
        elif config.clock != '24' and type(p[1]) == time:
            print(normalize(p[1]).strftime('%I %p'))
        else:
            print(normalize(p[1]))
        names['_'] = p[1]

def time2timedelta(n):
    return timedelta(hours=n.hour, minutes=n.minute, seconds=n.second) 

def p_expression_binop(p):
    '''expression : expression PLUS expression
                  | expression MINUS expression
                  '''
    if type(p[1]) == time and type(p[3]) == timedelta:
        p[1] = time2timedelta(p[1])

    if type(p[3]) == time and type(p[1]) == timedelta:
        p[3] = time2timedelta(p[3])

    if type(p[1]) == date and type(p[3]) == relativedelta:
        p[1] = datetime.combine(p[1], datetime.min.time())

    if type(p[3]) == date and type(p[1]) == relativedelta:
        p[3] = datetime.combine(p[3], datetime.min.time())

    if type(p[1]) == datetime and type(p[3]) == relativedelta:
        p[3] = datetime.now() - p[3]

    if type(p[3]) == datetime and type(p[1]) == relativedelta:
        p[1] = datetime.now() - p[1]

    if type(p[1]) == date and type(p[3]) == timedelta:
        p[1] = datetime.combine(p[1], datetime.min.time())

    if type(p[1]) == date and type(p[3]) == datetime:
        p[1] = datetime.combine(p[1], datetime.min.time())

    if type(p[3]) == date and type(p[1]) == datetime:
        p[3] = datetime.combine(p[3], datetime.min.time())

    if type(p[1]) == type(p[3]) == date and p[2] == '+':
        raise Exception(f'Can\'t add two dates: {p[1]} + {p[3]}')

    if p[1] is None or p[3] is None:
        raise Exception(f'In {p[2]} expr, p[1]={p[1]} and p[3]={p[3]}')

    if type(p[1]) == Weekday:
        p[1] = get_closest_week_day(str(p[1]))
    if type(p[3]) == Weekday:
        p[3] = get_closest_week_day(str(p[3]))
    if   p[2] == '+': 
        p[0] = p[1] + p[3]
    elif p[2] == '-': 
        p[0] = p[1] - p[3]

def p_expression_comparison(p):
    '''expression : expression GT expression
                  | expression LT expression
                  | expression GE expression
                  | expression LE expression
                  | expression EQ expression
                  | expression NE expression
                  '''

    if type(p[1]) == date and type(p[3]) == datetime:
        p[1] = datetime.combine(p[1], datetime.min.time())

    if type(p[3]) == date and type(p[1]) == datetime:
        p[3] = datetime.combine(p[3], datetime.min.time())

    try:
        if p[2] == '<':
            p[0] = p[1] < p[3]
        if p[2] == '>':
            p[0] = p[1] > p[3]
        if p[2] == '>=':
            p[0] = p[1] >= p[3]
        if p[2] == '<=':
            p[0] = p[1] <= p[3]
        if p[2] == '==':
            p[0] = p[1] == p[3] or (p[3] - p[1]).total_seconds() < .0001
        if p[2] == '!=':
            p[0] = p[1] != p[3]
        if type(p[1]) == datetime and \
                is_000(p[1]):
                    p[1] = p[1].date()
        if type(p[3]) == datetime and \
                is_000(p[3]):
                    p[3] = p[3].date()
    except TypeError as e:
        print(str(e))

def p_expression_funcall(p):
    '''expression : NAME point
                  | NAME DELTA
                  '''
    try:
        p[0] = names[p[1].lower()](p[2])
    except:
        raise Exception("Undefined name '%s'" % p[1])

def get_extremity_weekday_of_year(direction, weekday, year):
    if direction == 'last':
        target = date(year+1,1,1) - timedelta(days=1)
    elif direction == 'first':
        target = date(year,1,1)
    weekday_ix = [wd.lower() for wd in days].index(weekday)
    while target.weekday() != weekday_ix:
        if direction == 'first':
            target += timedelta(days=1)
        elif direction == 'last':
            target -= timedelta(days=1)
    return target

def get_extremity_weekday_of_basedate(direction, weekday, basedate):
    if direction == 'last':
        target = basedate + relativedelta(months=1, days=-1)
    elif direction == 'first':
        target = basedate
    weekday_ix = [wd.lower() for wd in days].index(weekday)
    while target.weekday() != weekday_ix:
        if direction == 'first':
            target += timedelta(days=1)
        elif direction == 'last':
            target -= timedelta(days=1)
    return target

def common_weekday_to_string(common_weekday):
    for ix,d in enumerate(relativedelta_days):
        if d == common_weekday:
            return days[ix]

def string_to_common_weekday(weekday):
    for ix,d in enumerate(days):
        if d.lower() == weekday.lower():
            return relativedelta_days[ix]

def cyclic(t, direction):
    cyclic_direction = datetime.now()
    for _ in range(7):
        cyclic_direction += timedelta(days=direction)
        if days[cyclic_direction.weekday()].lower() == str(t).lower():
            break
    return cyclic_direction.date()

def p_expression_relativedelta(p):
    '''expression : relativedelta IN MONTH
                  | relativedelta IN INTEGER
                  | relativedelta IN BASEDATE
                  | relativedelta 
                  '''
    direction, weekday = p[1]
    if len(p) > 2:
        if type(p[3]) == int:
            p[0] = get_extremity_weekday_of_year(direction, weekday, p[3])
        if type(p[3]) == datetime:
            p[0] = get_extremity_weekday_of_basedate(direction, weekday, p[3])
    elif direction in ['next', 'last']:
        leap = 1 if direction  == 'next' else -1
        p[0] = cyclic(weekday, leap)

def p_relativedelta_cyclic_op(p):
    'relativedelta : CYCLIC_OP WEEKDAY'
    p[0] = (p[1],p[2])

def delta_to_unit(delta, unit):
    total_seconds = delta.total_seconds()
    if unit == 'seconds':
        return total_seconds
    if unit == 'minutes':
        return total_seconds / 60
    if unit == 'hours':
        return total_seconds / 60 / 60
    if unit == 'days':
        return total_seconds / 60 / 60 / 24
    if unit == 'weeks':
        return total_seconds / 60 / 60 / 24 / 7

def p_expression_unit_until_point(p):
    '''expression : UNIT UNTIL point
                  | UNIT SINCE point
                  '''
    if p[1].lower() in unit_map.values():
        delta = p[3] - datetime.now()
        p[0] = delta_to_unit(delta, p[1].lower())
        if p[2] == 'since':
            p[0] = -p[0]
    else:
        raise Exception('Invalid syntax: {p[1]} {p[2]} {p[3]}')

def p_expression_generic(p):
    '''expression : DELTA 
                  | timestamp
                  | point
                  '''
    p[0] = p[1]

def p_timestamp_integer(p):
    'timestamp : INTEGER'
    p[0] = datetime.fromtimestamp(int(p[1]) if config.timestamp_unit == 'seconds' else int(p[1])/1000)

def p_statement_point_in_unit(p):
    'expression : expression IN UNIT'
    to_unix = p[3] == 'unix'
    if type(p[1]) == timedelta and to_unix:
        raise Exception('Can\'t convert timedelta to unix timestamp')
    if to_unix:
        p[0] = int(mktime(p[1].timetuple()))
    if p[3].lower() in unit_map.values():
        p[0] = delta_to_unit(p[1],p[3].lower())

def p_point(p):
    '''point : 
             | timestamp
             | DATETIME
             | BASEDATE
             | MONTH
             | WEEKDAY
             | YEAR
             '''
    p[0] = p[1]

def p_expression_get_attribute(p):
    'expression : expression PERIOD NAME'
    p[0] = names[p[3]](p[1])

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

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


import ply.yacc as yacc
yacc.yacc(errorlog=yacc.NullLogger())

def current_time_routine():
    names['n'] = names['now'] = names['N'] = names['NOW'] = datetime.now()
    names['today'] = names['T'] = names['t'] = names['TODAY'] = datetime.today().date()
    names['tomorrow'] = names['tm'] = names['TM'] = names['TOMORROW'] = datetime.today().date() + timedelta(days=1)
    names['yd'] = names['YD'] = datetime.today().date() - timedelta(days=1)

def interactive():
    import cmd
    class CmdParse(cmd.Cmd):
        prompt = ''
        commands = []
        def default(self, line):
            if line == 'EOF':
                exit(0)
            current_time_routine()
            yacc.parse(line)
            self.commands.append(line)
        def do_help(self, line):
            print(HELP)
        def do_exit(self, line):
            return True
    CmdParse().cmdloop()

if __name__ == '__main__':
    if len(sys.argv) > 1:
        if sys.argv[1] in ['-h', '--help']:
            print(HELP)
        else:
            current_time_routine()
            yacc.parse(' '.join(sys.argv[1:]))
    else:
        interactive()
