import sys, datetime, os, os.path, fnmatch, shutil, copy, re, subprocess
import time
from optparse import OptionParser, OptParseError, OptionError, OptionConflictError, BadOptionError, OptionValueError
from dateutil.parser import parse as duparse
from calendar import TextCalendar
from calendar import LocaleTextCalendar
from etm.v import version
from etm.rc import *

import locale
locale.setlocale(locale.LC_ALL, '')

logging = False
msg = []
has_icalendar = False
# for exporting to .ics
try:
    from icalendar import Calendar, Event, Todo, Journal, FreeBusy, UTC
    has_icalendar = True
except:
    has_icalendar = False

oneday = datetime.timedelta(days=1)
onesecond = datetime.timedelta(seconds=1)
oneminute = datetime.timedelta(minutes=1)
agenda_dflt = agenda
next_dflt = next
recent_dflt = recent
today = datetime.date.today()
lastyear = str(int(today.strftime("%Y"))-1)
thismonth = today.strftime("%m")
thisyear = today.strftime("%Y")
pastdate = today - recent*oneday
nextdate = pastdate + (next-1)*oneday
agendadate = today + (agenda-1)*oneday
verbose_toggle = verbose
date_fmt = "%Y-%m-%d"

monthname_fmt = "%b %d %y"

if use_ampm:
    timefmt = "%I:%M%p"
    datetime_fmt = "%Y-%m-%d %I:%M%p"
else:
    timefmt = "%H:%M"
    datetime_fmt = "%Y-%m-%d %H:%M"

if thisyear == '2009':
    copyright = '2009'
else:
    copyright = '2009-%s' % thisyear

all_off=os.popen("tput sgr0").read()

main_color_attrs = {
   -2 : main_dtls,
   -1 : main_mclr,
    0 : main_gclr,
    1 : main_pclr,
    2 : main_tclr,
    3 : main_sclr,
    4 : main_dclr,
    5 : main_hclr
}

print_color_attrs = {
   -2 : print_dtls,
   -1 : print_mclr,
    0 : print_gclr,
    1 : print_pclr,
    2 : print_tclr,
    3 : print_sclr,
    4 : print_dclr,
    5 : print_hclr
}

text_color_attrs = {
   -2 : "#555555",
   -1 : mclr,
    0 : gclr,
    1 : pclr,
    2 : tclr,
    3 : sclr,
    4 : dclr,
    5 : hclr
}

use_leader = False
codes = {}
codes['black'] = os.popen("tput setaf 0").read()
codes['red'] = os.popen("tput setaf 1").read()
codes['green']=os.popen("tput setaf 2").read()
codes['yellow']=os.popen("tput setaf 3").read()
codes['blue']=os.popen("tput setaf 4").read()
codes['magenta']=os.popen("tput setaf 5").read()
codes['cyan']=os.popen("tput setaf 6").read()
codes['white']=os.popen("tput setaf 7").read()
codes['gray']=os.popen("tput setaf 8").read()
codes['dim']=os.popen("tput sshm").read()
codes['bold']=os.popen("tput bold").read()
codes['special']=os.popen("tput sitm").read()


attrs = {
    -2 : codes[mclr],
    -1 : codes[mclr],
     0 : codes[gclr],
     1 : codes[pclr],
     2 : codes[tclr],
     3 : codes[sclr],
     4 : codes[dclr],
     5 : codes[hclr],
     7 : codes['white']
}

oneday = datetime.timedelta(days=1)
today = datetime.date.today()
startdate = datetime.date.today()
stopdate = startdate + (next-1)*oneday
part_regex = re.compile(r'^(\S)\s*(\S.*)')
etmdata_regex = re.compile(r'%s/?' % etmdata)
tab_regex = re.compile(r'\t')
days_regex = re.compile(r'^\s*([+-]?)\s*(\d+)')
parens_regex = re.compile(r'^\s*\((.*)\)\s*$')
leadingzero = re.compile(r'^0')
embeddedzero = re.compile(r'\s+0')
leadingspaces = re.compile(r'^(\s*)\S.*')
endline_regex = re.compile(r'[\n\r]', re.DOTALL)
done_regex = re.compile(r'.*\d{2}_%s.txt' % (done))
year_regex = re.compile(r'\!(\d{4})\!')
task_regex = re.compile(r'\s*([+\-\*\~])\s*(\S.*)', re.DOTALL)
project_regex = re.compile(r'\s*[^+\-\*\~@\s].*', re.DOTALL)
comment_regex = re.compile(r'\s*#')
# to match event lines in the alert queue:
evnt_regex = re.compile(r'\s+[0-9]')
# for the date calculator
calc_days_regex = re.compile(r'^(.+)\s+([-+])\s+(.+)(?=days?)')
calc_date_regex = re.compile(r'^(.+)\s+([-+])\s+(.+)(?!days?)')


special_keys = [ 'chrcode', 'do_next', 'type', 'file', 'j', 't']
common_keys = [ 'c', 'd', 'i', 'k', 'l', 'M', 'm', 'r', 'u', 'W', 'w', 'x' ]
project_keys = ['j'] + common_keys + ['b']
task_keys = common_keys + ['o', 'n', 'b', 'f']
event_keys = project_keys + ['s', 'e', 'a', 'n']
no_repeat_keys = ['d', 'b', 'c', 'k', 'n', 'f']
action_keys = ['c', 'd', 'p', 'k', 'n', 'j']
all_keys = [ 'a', 'b', 'c', 'd', 'e', 'f', 'i', 'k', 'l', 'M', 'm',
'n', 'o', 'p', 'j', 'r', 's', 'u', 'W', 'w', 'x', ]
sort_keys = [ 'd', 'b', 'p', 's', 'e', 'a', 'c', 'k', 'f', 'r', 'i',
'W', 'w', 'M', 'm', 'x', 'l', 'u', 'o', 'n', ]

alphalist = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

# for checks
date_keys = ['d', 'u', 'f']
list_date_keys = ['l', 'x']
# time_keys = ['s', 'e']
integer_keys = ['b', 'i', 'p']
list_integer_keys = ['a']
repeat_keys = ['l', 'm', 'M', 'u', 'w', 'W', 'x']

second_keys = [
'a', 'n', 'b', 'f', 'r', 'i', 'M', 'm', 'W', 'w', 'o', 'l', 'x' ]
g_keys = {}
# groupby date:
g_keys['d'] = ['c', 'j', 'k'] + second_keys 
# groupby context:
g_keys['c'] = ['e', 'j', 'k'] + second_keys 
# groupby keyword:
g_keys['k'] = ['e', 'c', 'j'] + second_keys 
# groupby project
g_keys['j'] = ['e', 'c', 'k'] + second_keys 

def sysinfo():
    from platform import python_version as pv
    from dateutil import __version__ as dv
    try:
        import wx
        wxv = "%s.%s.%s" % (wx.MAJOR_VERSION, 
                wx.MINOR_VERSION, wx.RELEASE_VERSION)
    except:
        wxv = "none"
    sysinfo = "etm version: %s; system: python %s; dateutil %s; wx(Python) %s" % (version, pv(), dv, wxv)
    return(sysinfo)

def newer():
    from urllib import urlopen, urlretrieve
    try:
        vstr = urlopen(
            "http://www.duke.edu/~dgraham/ETM/version.txt").read().strip()
        if int(version) < int(vstr): 
            return('A newer release, etm %s, is available.' % (vstr))
        else:
            return('etm %s is the newest version available.' % 
                    (vstr))

    except:
        return('Could not connect. Please try again later. %s')

dflts = {
    'j':   task,       # project title
    'c':   context,    # context
}

frequency_names = {
    'd' : 'DAILY',
    'w' : 'WEEKLY',
    'm' : 'MONTHLY',
    'y' : 'YEARLY',
    'l' : 'LIST'
}

by_names = {
    'w' : 'weekday',
    'W' : 'week',
    'm' : 'monthday',
    'M' : 'month'
}

date_group_names = {
        'b' : BeginBy,
        'n' : NoDueDate,
        'w' : WaitingPrior, 
        }


### Help text ###

date_event_text = [
'    date         @d a date. Required unless event is list-only repeating',
]

date_task_text = [
'    date         @d a date. Required for repeating tasks.',
]

common_text = [
'    context      @c string',
'    keywords     @k string or list of strings',
'    repeat       @r [dwmyl]: d)aily, w)eekly, m)onthly, y)early, l)ist',
'      interval   @i a positive integer. Default: 1',
'      until      @u a date. Default: forever',
'      weekday    @w MO, TU, ..., MO(-1) or [0-6] with 0=MO,',
'                 or a list of weekdays or weekday numbers',
'      week       @W an integer week number or list of week numbers',
'      monthday   @m an integer month day or list of monthdays',
'      month      @M an integer month number or list of month numbers',
'      include    @l a date or a list of (non-matching) dates to include',
'      exclude    @x a date or a list of (matching) dates to exclude',
]

list_text = """\
All dates are in YYYY-MM-DD format and all times in HH:MM[AP] format.
Fuzzy parsing is supported for both dates and times in all cases. All
lists must be comma separated and enclosed in parentheses. A list of
numbers can also be specified using the range operator, e.g., range(1,5)
instead of (1,2,3,4) or range(5,20,3) instead of (5,8,11,14,17).
""".split('\n')

textpad_keys = [
'Return an empty entry or an entry beginning with a period to cancel.',
'Editing keys:',
'    ^A: move to beginning of line        ^E: move to end of line ',
'    ^D: delete character under cursor    ^H: delete character backwards',
'    Return or ^G: process entry          ^K: delete to end of line',
'',
'Entry details:'
]

task_text = [
'[+-] &lt;task name&gt; [Options]',
'     where +: a parallel task; -: a series task',
'Options:',
] + date_task_text + common_text + [
'      overdue    @o k)eep, r)estart, s)kip. Default: k',
'    note         @n string',
'    begin        @b integer number of days before @d date',
'    finished     @f a date',
] # + list_text

project_text =['&lt;project name&gt; [Options]',
'    Options below become the default for each of the following,',
'    events, tasks and actions.',
'Options:'] + common_text # + list_text

event_text =['* &lt;event name&gt; [Options]',
'Options:',
] + date_event_text + common_text + [
'    starttime    @s a time',
'    endtime      @e a time or an integer number of minutes after',
'                 starttime. If starttime is set, the default is to',
'                 set endtime %s (EXTENT) minutes later.' % extent,
'    alerts       @a an integer or a list of integer minutes before',
'                 starttime',
'    note         @n string',
] # + list_text

action_text =['~ &lt;Description of action&gt; [Options]',
'Options:',
'    date         @d a date. Required.',
'    period       @p elapsed time in minutes. Required.',
'    context      @c string.',
'    keywords     @k string or list of strings. Keywords containing',
'                 colon(s), e.g., "x:y", "x:z", will be split to form',
'                 groups and subgroups when aggregating times.',
] # + list_text

usage = """\
usage: ...
"""

help = """\
               etm (Event and Task Manager) version %s.
           Copyright %s Daniel A Graham. All rights reserved.
    %s

Information:
    h       Show this help message
    c       Prompt for a date expression of the form 'date (+-) string'
            where 'string' is either a date or an integer followed
            by the word 'days' and return the result. E.g., 'dec 1 +
            90 days' or 'nov 30 - sep 1'. Note: '+' cannot be used
            when 'string' is a date.
    n       Display the latest available version of etm
    w       Start the wx(Python) based gui version of etm

Displays:
    b [B]   Display busy/free times using command line options [first
            prompting for options and displaying help information].
    l [L]   Display a list of events, actions and tasks using command line 
            options [first prompting for options and displaying help
            information].
    r [R]   Display a reckoning of time spent in events and actions 
            using command line options [first prompting for options and
            displaying help information].
    a [A]   Display the agenda using command line options [first prompting
            for options and displaying help information].
""" % (version, copyright, sysinfo())

guihelp = """%s
Changes:
    i  	    Start the timer for a new action, pause a running timer or
            restart a paused timer. Pressing 'I' stops the timer, prompts
            for a file number to record the entry and prompts for
            modifications of the entry.
    e  	    Create a new event. Prompt for file number, display event help
       	    and prompt for entry.
    t  	    Create a new task. Prompt for file number, display task help
       	    and prompt for entry.
    p  	    Create a new project. Prompt for file name, display project
       	    help and prompt for entry.
    d  	    Delete an existing task, event, action or action. Prompt for 
       	    the item number. Warning. This cannot be undone!
    f  	    Mark a task finished. Prompt for task number and, optionally, 
            a (fuzzy parsed) completion date to use instead of today's date.
            For a finished, non-repeating task, remove the finish date. For
            a repeating task, this action cannot be undone.
    m  	    Modify an existing task, event or action. Prompt for the number.
    o  	    Open an existing project for editing using an external editor.
            Display a numbered list of existing projects and prompt for the
            file number to use.
""" % help


### Parsers ###
class ETMOptParser(OptionParser):
    def error(self, m):
        global msg
        msg.append(m)

    def print_help(self, file=None):
        global msg
        msg.append(self.format_help())

bhelp =  """Date. Display tasks/events beginning with this date (fuzzy parsed) and continuing for the next DAYS days. Default: %default.
"""

dhelp = """Positive integer. The number of days to display. Default: %default.
"""

ehelp = """Date. Display tasks/events beginning with BEGIN and ending with this date (fuzzy parsed). Default: %default.
"""

### aparser ###
aparser = ETMOptParser(usage = '')
aparser.add_option("-d", action = "store",
    dest='days', type = int, default=agenda,
    help = dhelp)
aparser.add_option("-e", action = "store",
    dest='end', default=datetime.date.today()+(int(agenda)-1)*oneday,
    help = ehelp)
aparser.add_option("-f", 
    action="store",
    dest='find',
    help = """Regular expression. Include items containing FIND (ignoring case) in the task title or note within the BEGIN ~ END interval.""")
aparser.add_option("-c", action = "store",
    dest='context',
    help = """Regular expression. Include items with contexts matching CONTEXT (ignoring case) within the BEGIN ~ END interval.""")
aparser.add_option("-j", action = "store",
    dest='project',
    help = """Regular expression. Include items with projects matching PROJECT (ignoring case) within the BEGIN ~ END interval.""")
aparser.add_option("-k", action = "store",
    dest='keyword',
    help = """Regular expression. Include items with contexts matching KEYWORD (ignoring case) within the BEGIN ~ END interval.""")
aparser.add_option("-s", action = "store",
    dest='show', default='eta',
    help = """String. Include events, tasks and/or actions depending upon whether SHOW contains 'e', 't'/'T' and/or 'a'. Using 'T' rather than 't' limits the display of tasks to those which are unfinished. Default: %default.""")
aparser.add_option("-v", action = "store_true",
    dest='toggle_verbose',
    help = """Toggle displaying item details.""")

### lparser ###
lparser = ETMOptParser(usage = '')
lparser.add_option("-d", action = "store",
    dest='days', type = int, default=next,
    help = dhelp)
lparser.add_option("-b",  dest="begin",
    action="store", default=pastdate,
    help = bhelp)
lparser.add_option("-e", action = "store",
    dest='end', default=pastdate+(int(next)-1)*oneday,
    help = ehelp)
lparser.add_option("-g", action = "store",
    dest='groupby', default = 'd', choices = ['d', 'j', 'c', 'k'],
    help = """An element from [d,p,c,k] where:	                      \n
    d: group by date                        						  \n
    j: group by project				                				  \n
    c: group by context								                  \n
    k: group by keyword								                  \n
    Default: %default.""")
lparser.add_option("-f", 
    action="store",
    dest='find',
    help = """Regular expression. Include items containing FIND (ignoring case) in the task title or note within the BEGIN ~ END interval.""")
lparser.add_option("-c", action = "store",
    dest='context',
    help = """Regular expression. Include items with contexts matching CONTEXT (ignoring case) within the BEGIN ~ END interval.""")
lparser.add_option("-j", action = "store",
    dest='project',
    help = """Regular expression. Include items with projects matching PROJECT (ignoring case) within the BEGIN ~ END interval.""")
lparser.add_option("-k", action = "store",
    dest='keyword',
    help = """Regular expression. Include items with contexts matching KEYWORD (ignoring case) within the BEGIN ~ END interval.""")
lparser.add_option("-s", action = "store",
    dest='show', default='eta',
    help = """String. Include events, tasks and/or actions depending upon whether SHOW contains 'e', 't'/'T' and/or 'a'. Using 'T' rather than 't' limits the display of tasks to those which are unfinished. Default: %default.""")
lparser.add_option("-v", action = "store_true",
    dest='toggle_verbose',
    help = """Toggle displaying item details.""")
lparser.add_option("-x", action = "store",
    dest='export',
    help = """Export list view items in iCal format to file EXPORT.ics in %s.""" % etmical)

### bparser ###
bparser = ETMOptParser(usage = '')
bparser.add_option("-d", action = "store",
    dest='days', type = int, default=next, 
    help = dhelp)
bparser.add_option("-b",  dest="begin",
    action="store", default=pastdate,
    help = bhelp )
bparser.add_option("-e", action = "store",
    dest='end', default=nextdate,
    help = ehelp)
bparser.add_option("-s", action = "store",
    dest='slack',  type = int, default=slack,
    help = """Positive integer. Provide a buffer of SLACK minutes before and after busy periods when computing free periods. Default: %default.""")
bparser.add_option("-m", action = "store",
    dest='minimum',  type = int, default=minimum,
    help = """Positive integer. The minimum length in minutes for an unscheduled period to be displayed. Default: %default.""")
bparser.add_option("-o",  action = "store",
    dest='opening', default=opening,
    help = """Time. The opening or earliest time (fuzzy parsed) to be considered when displaying unscheduled periods. Default: %default.""")
bparser.add_option("-c", action = "store",
    dest='closing', default = closing,
    help = """Time. The closing or latest time (fuzzy parsed) to be considered when displaying unscheduled periods. Default: %default.""")
bparser.add_option("-i", action = "store",
    dest='include', default = include,
    help = """String containing one or more of the letters 'b' (include busy time bars, 'B' (include busy times), 'f' (include free time bars) and/or 'F' (include free times). Default: %default.""")

### rparser ###
rparser = ETMOptParser(usage = '')
rparser.add_option("-d", action = "store",
    dest='days', type = int, default = next,
    help = dhelp)
rparser.add_option("-b",  dest="begin",
    action="store", default=pastdate,
    help = bhelp )
rparser.add_option("-e", action = "store",
    dest='end', default=nextdate,
    help = ehelp)
rparser.add_option("-f", 
    action="store",
    dest='find',
    help = """Regular expression. Include items containing FIND (ignoring case) in the task title or note within the BEGIN ~ END interval.""")
rparser.add_option("-c", action = "store",
    dest='context',
    help = """Regular expression. Include items with contexts matching CONTEXT (ignoring case) within the BEGIN ~ END interval.""")
rparser.add_option("-j", action = "store",
    dest='project',
    help = """Regular expression. Include items with projects matching PROJECT (ignoring case) within the BEGIN ~ END interval.""")
rparser.add_option("-k", action = "store",
    dest='keyword',
    help = """Regular expression. Include items with contexts matching KEYWORD (ignoring case) within the BEGIN ~ END interval.""")

rparser.add_option("-C", action = "store",
    dest='c_position', type=int, default = c_position,
    help = """Integer. Subtotal by category if C_POSITION is positive and in an order corresponding to C_POSITION. Default %default.""")
rparser.add_option("-D", action = "store",
    dest='d_position', type=int, default = d_position,
    help = """Integer. Subtotal by date if D_POSITION is positive and in an order corresponding to D_POSITION. Default %default.""")
rparser.add_option("-K", action = "store",
    dest='k_level', type=int, default = k_level,
    help = """Integer. Subtotal by keywords to a depth corresponding to the value of K_LEVEL if positive. E.g., if K_LEVEL is 2, and there is an item with keyword \'a:b:c\', then subtotals corresponding to \'a\' and \'a:b\' would be formed. Default %default.""")
rparser.add_option("-I", action = "store_true", dest='itemize',
    help = """True/False. Subtotal by title if True. Default: False.""")

parserHash = {
        'a' : aparser,
        'l' : lparser,
        'b' : bparser,
        'r' : rparser,
        }

nameHash = {
        'a' : 'agenda options',
        'l' : 'list options',
        'b' : 'busy/free options',
        'r' : 'reckoning options'
        }


if logging:
    etmlog = os.path.join(etmdir, 'etm.log')
    #  clear the log
    fo = open(etmlog, 'w')
    fo.close()

def logmsg(msg):
    if logging:
        fo = open(etmlog, 'a')
        fo.write("%s\n" % str(msg))
        fo.close()
    else:
        pass

def parse(str):
    return duparse(str, dayfirst=False, yearfirst=False)

def num2alpha(integer):
    """Convert 1, 2, 3, ..., 27, 28, 29 ... 18278 to a, b, c, ..., aa, ab,
    ac ... zzz. Three alphabetic 'digits' thus allow for intgers from 1
    through  26 + 26^2 + 26^3 = 18,278. Note: repetitions of an item use
    the same id number since all correspond to the same file and linenumber.
    """
    num = int(integer)
    al = []
    while 1:
        al.insert(0, alphalist[num % 26 - 1])
        num = num/26
        if num == 0:
            break
    return ''.join(al)

def m2h(m):
    """
    Return hours and minutes if hours_minutes is true and otherwise hours
    and tenths.
    """
    m = int(m)
    if hours_minutes:
        return "%d:%02d" % (m/60, m%60)
    else:
        if m%6 > 0:
            return "%d.%dh" % (m/60, (m%60+6)/6)
        else:
            return "%d.%dh" % (m/60, (m%60)/6)

def l2u(l_date, l_time="0:00"):
    """Make sure that the dst flag is -1 -- this tells mktime to take daylight
    savings into account"""
    l_dto = parse("%s %s" % (l_date, l_time))
    l_dts = l_dto.timetuple()
    l_secs = time.mktime(l_dts)
    u = time.gmtime(l_secs)
    if l_time != "0:00":
        return datetime.datetime(u.tm_year, u.tm_mon, u.tm_mday, u.tm_hour, u.tm_min, u.tm_sec,
            tzinfo=UTC)
    else:
        return datetime.date(u.tm_year, u.tm_mon, u.tm_mday)

def has(hash, key):
    "Return true if key in hash and hash[key] != None"
    #  return(key in hash and hash[key] != None)
    return(key in hash and hash[key])

def cal():
    c = LocaleTextCalendar(week_begin, '')
    cal = []
    y = int(today.strftime("%Y"))
    m = int(today.strftime("%m"))
    if m == 1:
        m = 12
        y -= 1
    else:
        m -= 1
    for i in range(12):
        cal.append(c.formatmonth(y,m).split('\n'))
        m += 1
        if m > 12:
            y += 1
            m = 1
    s = []
    for r in range(0,12,3):
        l = max(len(cal[r]), len(cal[r+1]), len(cal[r+2]))
        for i in range(3):
            if len(cal[r+i]) < l:
                for j in range(len(cal[r+i]), l+1):
                    cal[r+i].append('')
        for j in range(l):
            s.append(("  %-20s    %-20s    %-20s" %
                (cal[r][j], cal[r+1][j], cal[r+2][j])).encode('utf8'))
    return s

def format_date(date, fmt):
    global msg
    if type(date) is str:
        try:
            d = parse(date).date()
            return d.strftime(fmt)
        except:
            msg.append("Could not format date '%s'" % date)
            return(date)
    else:
        return(date.strftime(fmt))

def parse_date(date):
    # return datetime.date
    global msg
    if type(date) in [str, int]:
        try:
            date = parse('%s' % date)
        except:
            msg.append("Could not parse date '%s'" % date)
            return(today)
    if type(date) is datetime.datetime:
        return(date.date())
    elif type(date) is datetime.date:
        return(date)
    else:
        return(today)

def date_calculator(s):
    """process a date expression
    date - date     return days between dates
    date + date     return error
    date - n day(s) return date
    date + n day(s) return date
    """
    str = s.strip()
    time = datetime.time(0,0,0,0)
    m = calc_days_regex.match(str)
    if m:
        # using days
        #  print('days', str,  m.groups())
        a = m.group(1).strip()
        v = m.group(2).strip()
        n = m.group(3).strip()
        days = int(n)
        try:
            a_datetime = datetime.datetime.combine(parse_date(a),time)
        except:
            return(
                "error processing '%s': could not parse date '%s'" % (str, a))
        a_fmt = a_datetime.strftime("%Y-%m-%d")
        if v == '+':
            res_dt = a_datetime + days*oneday
        else:
            res_dt = a_datetime - days*oneday
        res = res_dt.strftime("%Y-%m-%d")
        return("%s %s %s days = %s" % (a_fmt, v, days, res))
    m = calc_date_regex.match(str)
    if m:
        # using date
        #  print('date', str,  m.groups())
        a = m.group(1).strip()
        v = m.group(2).strip()
        n = m.group(3).strip()
        # n must be a date
        try:
            a_datetime = datetime.datetime.combine(parse_date(a),time)
        except:
            return(
                "error processing '%s': could not parse date '%s'" % (str, a))
        a_fmt = a_datetime.strftime("%Y-%m-%d")
        if v == '+':
            return(
                "error processing '%s': '+' can only be used with 'days'" %
                    (str))
        else:
            try:
                b_datetime = datetime.datetime.combine(parse_date(n),time)
            except:
                return(
                    "error processing '%s': could not parse date '%s'" %
                        (str, n))
            dt1 = max(a_datetime, b_datetime)
            dt2 = min(a_datetime, b_datetime)
            datedelta = dt1 - dt2
            num_days = datedelta.days
            if num_days == 1:
                d = 'day'
            else:
                d = 'days'
            return("%s - %s = %s %s " % (dt1.strftime("%Y-%m-%d"),
                dt2.strftime("%Y-%m-%d"), num_days, d))
    return("error parsing '%s'. (The '+' or '-' should have a space on each side.)" % str)


def year2string(startyear, endyear):
    """compute difference and append suffix"""
    diff = int(endyear) - int(startyear)
    suffix = 'th'
    if diff < 4 or diff > 20:
        if diff%10 == 1:
            suffix = 'st'
        elif diff%10 == 2:
            suffix = 'nd'
        elif diff%10 == 3:
            suffix = 'rd'
    return "%d%s" % (diff, suffix)

def sort_listofhashes_by_field(listofhashes, *fields):
    lst = []
    thefields = []
    for item in fields:
        thefields.append(item)
    if thefields[0] == 'd':
        thefields.append('B')
    elif thefields[0] == 'B':
        thefields.append('d')
    thefields.append('S')
    for i in range(len(listofhashes)):
        try:
            t = [listofhashes[i][field] for field in thefields]
            lst.append([t, i])
        except:
            for hash in listofhashes:
                print(hash)
            print(fields)
            print(sys.exc_info())
            raise Exception()
    lst.sort()
    indices = [l[1] for l in lst]
    return [listofhashes[i] for i in indices]


def get_args(cmd):
    "Show the appropriate help screen and prompt for options."
    # The data version. There are separate curses and wx versions.
    global msg
    cmdlc = cmd.lower()
    get_help(cmdlc) 
    print("\n".join(msg))
    argstr = raw_input('%s: ' % nameHash[cmdlc])
    args = argstr.split(' ')
    return(args)

def get_help(cmd):
    "Return the relevant help screen for cmd."
    global msg
    cmdlc = cmd.lower()
    options = None
    args = ['-h']
    try:
        (options, args) = parserHash[cmdlc].parse_args(args)
    except:
        pass


def parse_args(cmd, args=[], start=today, stop=agendadate):
    """Parse args using the relevant parser for cmd. Perform consistency
    checks for begin, days and end and for opening and closing."""
    cmdlc = cmd.lower()
    cmduc = cmd.upper()
    options = None
    errors = []
    if cmdlc in ['l', 'b', 'r', 'a']:
        if cmduc == cmd:
            args = get_args(cmdlc) 
        try:
            (def_opts, toss) = parserHash[cmdlc].parse_args([])
            try:
                (options, toss) = parserHash[cmdlc].parse_args(args)
            except:
                errors = ["could not parse %s using parserHash['%s']" % 
                        (args, cmdlc)]
                return({}, errors)

            #  print(options.__dict__)
            #  print(c_position, d_position, k_level)
            new_begin = has(options.__dict__, 'begin') and \
                    type(options.__dict__['begin']) == str
            new_end = has(options.__dict__, 'end') and \
                    type(options.__dict__['end']) == str
            options.view = cmdlc

            new_days = False
            def_end = parse_date(def_opts.end)
            #  def_end = parse_date(def_opts.end).strftime(date_fmt)
            try:
                days = int(options.days)
                options.days = int(options.days)
                def_days = int(def_opts.days)
                assert days > 0
            except:
                msg.append('option -d: invalid value (%s) for DAYS' 
                        % options.days)
                raise OptionValueError('DAYS must be a positive integer.')
            # agenda
            if cmdlc == 'a':
                def_begin = today
                options.begin = today
                options.groupby = 'd'
                options.end = parse_date(options.end)
            else:
                options.begin = parse_date(options.begin)
                options.end = parse_date(options.end)
                def_begin = parse_date(def_opts.begin)

            if days != def_days:
                new_days = True
            if cmdlc == 'a' and new_end and new_days:
                errors.append('At most one of END and DAYS can be set')
                raise OptionValueError(
                        'At most one of END and DAYS can be set')
            elif new_begin and new_end and new_days:
                errors.append('At most two of BEGIN, END and DAYS can be set')
                raise OptionValueError(
                        'At most two of BEGIN, END and DAYS can be set')
            if (new_begin or cmdlc == 'a') and not new_end:
                options.end = options.begin + (days-1)*oneday
            elif new_end and not (new_begin or cmdlc == 'a'):
                options.begin = options.end - (days-1)*oneday
            elif not (new_begin or new_end):
                options.end = options.begin + (days-1)*oneday

            options.begin = parse_date(options.begin)
            options.end = parse_date(options.end)
            if options.begin > options.end:
                errors.append('option conflict: END (%s) occurs before BEGIN (%s)' % (options.end, options.begin))
                raise OptionValueError('end must not be sooner than begin')
            # opening, closing, minimum (for busy/free)
            if parserHash[cmdlc].has_option('-o') and parserHash[cmdlc].has_option('-c'):
                opening = parse(options.opening)
                closing = parse(options.closing)
                earliest_closing = opening + options.minimum*oneminute
                options.opening = format_date(opening, timefmt)
                options.closing = format_date(closing, timefmt)
                if earliest_closing > closing:
                    errors.append('option conflict: CLOSING (%s) must occur at least MINIMUM (%s) minutes after OPENING (%s)' % (options.closing, options.minimum, options.opening)) 
                    raise OptionValueError(
                        'closing must not be later than opening')

            if parserHash[cmdlc].has_option('-v') and \
                    options.toggle_verbose:
                options.verbose_toggle = not verbose_toggle
            else:
                options.verbose_toggle = verbose_toggle

        except:
            e = sys.exc_info()
            errors.append(e)

    return(options.__dict__, errors)

