import sys, datetime, time, os, os.path, fnmatch, shutil, re, subprocess
from optparse import OptionParser, OptParseError, OptionError, OptionConflictError, BadOptionError, OptionValueError
from dateutil.parser import parse as duparse
from dateutil.tz import tzlocal, tzutc, gettz
#  from calendar import TextCalendar
#  from calendar import LocaleTextCalendar
from etmVersion import version
from copy import deepcopy
from textwrap import wrap as text_wrap
from platform import system
from etmRC import *
etm_redirect = os.path.join(etmdir, 'etm.log')

#  import locale
#  try:
    #  # try using the user's default settings.
    #  locale.setlocale(locale.LC_ALL, '')
#  except:
    #  # use the current setting for locale.LC_ALL
    #  locale.setlocale(locale.LC_ALL, None)

soon = 7

sort_str = u'dbsezaAgckltpUriCSWwMm-+uofn'
sort_keys = [x for x in sort_str]

fieldNum = dict(
        y='0',      # y)ear
        m='1',      # m)onth
        d='2',      # d)ay
        s='3',      # s)ort num
        e='5',      # e)xtent minutes
        p='6',      # p)riority
        w='7',      # w)eek number (in year)
        q='8',      # q)uarter number
        c='9',      # c)ontext
        k1='10',    # k)eyword 1
        k2='11',    # k)eyword 2
        k3='12',    # k)eyword 3 and beyond
        l='13',     # l)ocation
        T='17',     # t)itle, description of item
        I='19',     # i)d, file name and lines
        u='21',     # u)ser
        P='22',     # p)roject name
        t='23',     # t)ag
        n='24',     # n)ote
        )

logging = False
parser_msg = []
attrs = {}
all_off = ''
def get_attrs():
    #  global attrs, all_off
    codes = {}
    all_off=os.popen("tput sgr0").read()
    if all_off:
        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 = {
            sortOrder['allday']     : codes[allday_event_color],
            sortOrder['event']      : codes[event_color],
            sortOrder['reminder']   : codes[reminder_color],
            sortOrder['action']     : codes[action_color],
            sortOrder['pastdue']    : codes[task_pastdue_color],
            sortOrder['task']       : codes[task_color],
            sortOrder['undated']    : codes[task_undated_color],
            sortOrder['waiting']    : codes[task_waiting_color],
            sortOrder['begin']      : codes[task_beginby_color],
            sortOrder['finished']   : codes[task_finished_color],
            sortOrder['note']       : codes[note_color],
            11                      : codes[header_1_color],
            12                      : codes[header_2_color],
            13                      : codes[header_3_color],
            14                      : codes[detail_color],
        }
    else:
        all_off = ''
        attrs = {}
        for i in range(0, 15):
            attrs[i] = ''
    return(all_off, attrs, codes)

# for goto list
platform = system()
def OpenWithDefault(path):
    if platform in ('Windows', 'Microsoft'):
        os.startfile(path)
    elif platform == 'Darwin':
        import subprocess
        subprocess.Popen('/usr/bin/open' + " %s" % path, shell = True)
    else:
        import subprocess
        subprocess.Popen('xdg-open' + " %s" % path, shell = True)


# for exporting to .ics
has_vobject = False
try:
    import vobject
    has_vobject = True
except:
    has_vobject = False

def getToday():
    d = datetime.datetime.today()
    dt_today = datetime.datetime.combine(d, datetime.time(due_hour, 0, 0))
    dtz_today = dt_today.replace(tzinfo=tzlocal())
    today = tuple(map(int, dtz_today.strftime("%Y,%m,%d").split(',')))
    soondate = tuple(map(int, (dtz_today + soon * oneday).strftime("%Y,%m,%d").split(',')))
    return(dtz_today, today, soondate)

current_day = None
def get_today(td=None):
    """If called with a date and current_day = None, then return current_day on this and subsequent calls until called again with a non-null argument."""
    global current_day
    if current_day:
        if not td:
            return(current_day)
        else:
            current_day = None
            return(datetime.datetime.now())
    else:
        if td:
            current_day = parse_date(td)
            return(current_day)
        else:
            return(datetime.datetime.now())

oneday = datetime.timedelta(days=1)
onesecond = datetime.timedelta(seconds=1)
oneminute = datetime.timedelta(minutes=1)
today = get_today()
lastyear = str(int(today.strftime("%Y"))-1)
thismonth = today.strftime("%m")
thisyear = today.strftime("%Y")
date_fmt = "%Y-%m-%d"
today_tup = tuple(map(int, today.strftime(date_fmt).split('-')))
soon_tup = tuple(map(int, (today + soon*oneday).strftime(date_fmt).split('-')))

monthname_fmt = "%b %d %y"

status_fmt = "%a %b %d"
if use_ampm:
    tdyfmt = "%I:%M:%S%p %a, %b %d"
    timefmt = "%I:%M%p"
    datetime_fmt = "%Y-%m-%d %I:%M%p"
else:
    tdyfmt = "%H:%M:%S %a, %b %d"
    timefmt = "%H:%M"
    datetime_fmt = "%Y-%m-%d %H:%M"

# save times and dates in 24 hour format
time_24fmt = "%H:%M"
datetime_24fmt = "%Y-%m-%d %H:%M"
tdy_24fmt = "%H:%M:%S %a, %b %d"

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

sortItems = [
        'allday',   # 0
        'event',    # 1 (start time with extent)
        'reminder', # 2 (start time without extent)
        'pastdue',  # 3
        'task',     # 4 (not finished, pastdue, undated or waiting)
        'waiting',  # 5
        'undated',  # 6
        'begin',    # 7
        'action',   # 8
        'finished', # 9
        'note',     #10
        ]

sortOrder = {}
for i in range(len(sortItems)):
    sortOrder[sortItems[i]] = i

colors = {
    sortOrder['allday']    : gui_allday,
    sortOrder['reminder']  : gui_reminder,
    sortOrder['action']    : gui_action,
    sortOrder['event']     : gui_event,
    sortOrder['pastdue']   : gui_pastdue,
    sortOrder['waiting']   : gui_waiting,
    sortOrder['task']      : gui_task,
    sortOrder['undated']   : gui_undated,
    sortOrder['begin']     : gui_begin,
    sortOrder['finished']  : gui_finished,
    sortOrder['note']      : gui_note,
    11                     : gui_header1,
    12                     : gui_header2,
    13                     : gui_header3,
    14                     : gui_details,
    }

char2Mode = {
        'f' : 'finish',
        'u' : 'unfinish',
        'd' : 'delete',
        'j' : 'jump',
        'm' : 'move',
        'c' : 'clone',
        'o' : 'outline',
        'b' : 'busy',
        'p' : 'open',
        'P' : 'project',
        '~' : 'actions',
        'A' : 'Actions',
        '*' : 'events',
        '!' : 'notes',
        '-' : 'tasks',
        '+' : 'tasks',
        }

ltr2Char = {
        'a' : '~',
        'e' : '*',
        'n' : '!',
        't' : '-',
        '~' : '~',
        '*' : '*',
        '!' : '!',
        '-' : '-',
        '+' : '+',
        }

mode2Description = {
        'clone' : 'creating clone of item',
        'finish' : 'adding finish date to task',
        'unfinish' : 'removing last finish date from task',
        }

at_regex = re.compile(r'\s+@')
int_regex = re.compile(r'^\s*\d+')
#  e_regex = re.compile(r'\s*@e\s*\+[\S]+\s*')
oneday = datetime.timedelta(days=1)
today = get_today()
parens_regex = re.compile(r'^\s*\((.*)\)\s*$')
leadingzero = re.compile(r'^0')
#  no_regex = re.compile(r'^no(ne)?$') # for skipping details with value no or none
no_regex = re.compile(r'^no(ne| .*)$') # skip details with value 'no ...' or 'none'
embeddedzero = re.compile(r'(?<=[\s\(])0')
embeddedampm = re.compile(r'(?<=[AP])M')
year_regex = re.compile(r'\!(\d{4})\!')
item_regex = re.compile(r'^\s*(\*|\&|\~|\+|\-|\!|\_+|\.+|\$)\s+(\S.*)$')
range_regex = re.compile(r'^(.*\b)(range\([\d\, ]+\))(.*)$')
comment_regex = re.compile(r'\s*#')
# for completion
multiple_whitespace_regex = re.compile(r'\s{2,}')

display_template_regex = re.compile(r'.*(\s+\-[a-zA-Z].*$)+?')
display_full_regex = re.compile(r'^([ob].*)')
item_template_regex = re.compile(r'.*(\s+@[a-zA-Z].*$)+?')
item_full_regex = re.compile(r'^([~\*\!\-\+].*)')

# for the date calculator
calc_days_regex = re.compile(r'^(.+)\s+([-+])\s+(.+)(?=days?)')
# for relative date parsing
rel_date_regex = re.compile(r'^([-+])([0-9]+)')

# -b -1/1 -e -0/1 would include all days in the previous month
# -b -0/1 -e +1/1 would include all days in the current month
# -b +1/1 -e +2/1 would include all days in the next month
rel_monthdate_regex = re.compile(r'^([-+])([0-9]+)/([0-9]+)')

# for the date calculator
calc_date_regex = re.compile(r'^(.+)\s+([-+])\s+(.+)(?!days?)')
ws_regex = re.compile(r'\s+')
leadingspace_regex = re.compile(r'^([ \t]+)')  # leading space or tab characters

opt_keys = {}
opt_keys['m'] = ['groupby', 'begin', 'context', 'keyword', 'user', 'project', 'omit', 'search', 'file']
opt_keys['i'] = ['groupby', 'begin', 'days', 'end', 'context', 'keyword', 'user', 'project', 'omit', 'vcal', 'search', 'file', 'include_item']
opt_keys['b'] = ['begin', 'days', 'd', 'include_busy', 'context', 'keyword', 'user', 'project', 'wrap', 'minimum', 'minutes', 'opening', 'closing', 'search', 'file']
opt_keys['l'] = ['begin', 'days', 'end', 'context', 'user', 'keyword', 'project', 'include_ledger', 'omit', 'search', 'file']
opt_keys['d'] = ['context', 'keyword', 'user', 'project', 'omit', 'search', 'file']

short_opts = {
    'groupby'           : 'g',
    'begin_date'        : 'b',
    'context'           : 'c',
    'details'           : 'd',
    'end_date'          : 'e',
    'keyword'           : 'k',
    'user'              : 'u',
    'location'          : 'l',
    'project'           : 'p',
    'omit'              : 'o',
    'ical'              : 'i',
    'values'            : 'v',
    'wrap'              : 'w',
    'search'            : 's',
    'file'              : 'f',
    'minimum'           : 'm',
    'minutes'           : 'H',
    'opening'           : 'O',
    'closing'           : 'C',
    'totalsfirst'       : 'T',
    'include'           : 'i',
}

show_opts = {
    'context'           : 'c',
    'keyword'           : 'k',
    'user'              : 'u',
    'location'          : 'l',
    'project'           : 'p',
    'omit'              : 'o',
    'wrap'              : 'w',
    'file'              : 'f',
    'minimum'           : 'm',
    'minutes'           : 'H',
    'opening'           : 'O',
    'closing'           : 'C',
    'include'           : 'i',
}
special_keys = [u'DS', u'DT', u'ID']
common_keys = [ 'c', 'd', 'g', 'i', 'k', 'l', 'M', 'm', 'r', 'u', 'U', 'W', 'w', 'x', 'z' ]
project_keys = ['j'] + common_keys + ['b']
task_keys = common_keys + ['o', 'n', 'b', 'p', 'f', 'prereq']
event_keys = project_keys + ['s', 'e', 'a', 'A', 'n']
action_keys = ['c', 'd', 'g', 'e', 'k', 'n', 'j', 'U', 'z']
note_keys = ['c', 'd', 'k', 'n', 'j', 'U', 'z']

char2Keys = {
        'A' : action_keys,
        '~' : action_keys,
        '*' : event_keys,
        '!' : note_keys,
        '-' : task_keys,
        '+' : task_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 = "platform: %s; python %s; dateutil %s; wx(Python) %s" % (sys.platform, pv(), dv, wxv)
    return(sysinfo)

#  def etminfo():
    #  etminfo = "etmdir: %s; etmdata: %s" % (etmdir, etmdata)
    #  return(etminfo)

def newer():
    global version
    import socket
    timeout = 10
    socket.setdefaulttimeout(timeout)
    # strip the '-x' from experimental versions
    version = (version.split('-'))[0]
    from urllib2 import Request, urlopen, URLError
    req = Request("http://www.duke.edu/~dgraham/ETM/version.txt")
    try:
        response = urlopen(req)
    except URLError, e:
        if hasattr(e, 'reason'):
            msg = """\
We failed to reach a server.
Reason: %s.""" % e.reason
        elif hasattr(e, 'code'):
            msg = """\
The server couldn\'t fulfill the request.
Error code: %s.""" % e.code
        return(0, msg)
    else:
        # everything is fine
        vstr = response.read().strip()
        if int(version) < int(vstr):
            return(1, 'A new version (%s), is available.' % (vstr))
        else:
            return(1, 'You are using the latest version.')

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

def normalize_whitespace(s):
    return(unicode(multiple_whitespace_regex.sub(' ', s)))

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

description = """\
etm provides a format for using simple text files to store
action, event, note, and task items, a command line interface
for viewing items in a variety of convenient ways and a wx
(python) based GUI for creating and modifying items as well
as viewing them.
"""

license = """\
This program is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation;
either version 2 of the License, or (at your option) any
later version. [ http://www.gnu.org/licenses/gpl.html ]

This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more
details.
"""

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

optionParms = {}

# parms = switch, action, dest, default, help

#  optionParms['begin_date'] = ("-b", "store", "begin_date", today_tup,
optionParms['begin_date'] = ("-b", "store", "begin_date", None,
"""Date. Display items beginning with this date (fuzzy parsed) and continuing
until END_DATE. Relative day and month expressions can also be used so that,
for example, '-b -14' would begin 14 days before the current date and '-b
-1/1' would begin on the first day of the previous month. Default: today.""")

optionParms['end_date'] = ("-e", "store", "end_date", None, # soon_tup,
"""Date. Display items beginning with BEGIN and ending with this date (fuzzy
parsed). As with 'begin_date' relative month expressions can be used so that,
for example, '-b -1/1  -e +1/1' would include all items from the last and
current months. Default: BEGIN plus 6 days. """)

optionParms['file'] = ("-f", "store", 'file', None,
"""Regular expression. Include items with project file names matching FILE (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !FILE rather than FILE, to include items which do NOT have file names matching FILE.""")

optionParms['priority'] = ("-p", "store", 'priority', None,
"""Regular expression. Include items with project titles matching PRIORITY within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !PRIORITY rather than PRIORITY, to include items which do NOT have priorities matching PRIORITY.""")

optionParms['project'] = ("-P", "store", 'project', None,
"""Regular expression. Include items with project titles matching PROJECT (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !PROJECT rather than PROJECT, to include items which do NOT have project titles matching PROJECT.""")

optionParms['context'] =("-c", "store", 'context', None,
    """Regular expression. Include items with contexts matching CONTEXT (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !CONTEXT rather than CONTEXT, to include items which do NOT have contexts matching CONTEXT.""")

optionParms['tag'] =("-t", "append", 'tag', None,
    """Regular expression. Include items with tags matching TAG (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !TAG rather than TAG, to include items which do NOT have tags matching TAG. This switch can be used more than once, e.g., use '-t tag 1 -t tag 2' to match items with tags that match 'tag 1' and 'tag 2'.""")

optionParms['keyword'] = ("-k", "store", 'keyword', None,
    """Regular expression. Include items with contexts matching KEYWORD (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !KEYWORD rather than KEYWORD, to include items which do NOT have keywords matching KEYWORD.""")

optionParms['location'] = ("-l", "store", 'location', None,
"""Regular expression. Include items with location matching LOCATION (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !LOCATION rather than LOCATION, to include items which do NOT match LOCATION.""")

optionParms['user'] = ("-u", "store", 'user', None,
"""Regular expression. Include items with user matching USER (ignoring case) within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !USER rather than USER, to include items which do NOT match USER.""")

optionParms['search'] = ("-s", "store", 'search', None,
   """Regular expression. Include items containing SEARCH (ignoring case) in the task title or note within the BEGIN ~ END interval. Prepend an exclamation mark, i.e., use !SEARCH rather than SEARCH, to include items which do NOT have titles or notes matching SEARCH.""")

optionParms['totalsfirst'] =("-T", "store_true", 'totalsfirst', False,
    """Boolian. Display minute totals at the beginning rather than the end of the line.""")

optionParms['omit'] = ("-o", "store", 'omit', None,
     """string. show/hide a)ctions, task b)egin dates, all d)ay events, scheduled  e)vents, f)inished tasks, n)otes, r)eminders, all t)asks, u)ndated tasks and/or w)aiting tasks depending upon whether omit contains 'a', 'b', 'd', 'e', 'f', 'n', 'r', 't', 'u' and/or 'w' and begins with '!' (show) or does not being with '!' (hide). default: %default.""")

optionParms['groupby'] = ("-g", "store", 'cols', '((y, m, d),)',
"""A tuple of elements from y (year), m (month), d (day), s (sort or type number), e (extent minutes), w (yearly week number), q (quarter number), c (context), k1 (keyword 1), k2 (keyword 2), k3 (keyword 3), l (location), n (item name), f (file name and line numbers), u (user), p (project name) and i (id). For example, the default, -g ((y, m, d),), sorts by year, month and day together to give output such as                                                                    \n
|   Fri Apr 1 2011                                                           \n
|       items for April 1                                                    \n
|   Sat Apr 2 2011                                                           \n
|       items for April 2                                                    \n
|   ...                                                                      \n
As another example, -g ((y, q), m, d), would sort by year and quarter, then month and finally day to give output such
as
                                                                             \n
|   2011 2nd quarter                                                         \n
|       Apr                                                                  \n
|           Fri 1                                                            \n
|               items for April 1                                            \n
|           Sat 2                                                            \n
|               items for April 2                                            \n
|   ...                                                                      \n
A final option is to group by file path using '-g F'. Note that if 'F' should not be combined with any other groupby options.
""")

optionParms['values'] = ("-x", "store", 'values', None,
"""Comma separated list of field keys. Export displayed items in CSV (comma separted values) format to export.csv in the 'export' directory specified in the etm rc file. Values exported for each item include 1) item id, 2) item type number, 3) item description (title) and, in order, values corresponding to the keys in VALUES. Possible keys include y (year), m (month), d (day), e (extent minutes), p (priority), w (week number),  q (quarter number),  c (context),  k1, k2, k3, (keywords 1, 2 and beyond), l (location), u (user), P (project name), t (tags) and n (note). """)

optionParms['vcal'] = ("-v", "store_true", 'vcal', None,
"""Export items in vCal/iCal format to export.ics in the directory specified by 'export' in the etm rc file.""")

optionParms['details'] = ("-d", "store", 'details', 1,
"""String. Controls the display of item details. With '-d 0', item details would not be displayed. With '-d 1' (the default), the prefix and title (description) would be displayed. With '-d len', for example, a second details line would be appended displaying the item l)ocation, e)xtent and n)ote entries.""")

optionParms['wrap'] = ("-w",  "store", 'wrap', wrap,
"""Positive integer. Provide a buffer of WRAP minutes before and after busy
periods when computing free periods. Default: %default.""")

optionParms['minimum'] = ("-m", "store", 'minimum', minimum,
 """Positive integer. The minimum length in minutes for an unscheduled period
 to be displayed. Default: %default.""")

optionParms['opening'] = ("-O", "store", 'opening', opening,
 """Time. The opening or earliest time (fuzzy parsed) to be considered when
 displaying unscheduled periods. Default: %default.""")

optionParms['closing'] = ("-C", "store", 'closing', closing,
 """Time. The closing or latest time (fuzzy parsed) to be considered when
 displaying unscheduled periods. Default: %default.""")

optionParms['include'] = ("-i", "store", 'include', include,
"""String containing one or more characters from B (busy time bars), b (busy times), F (free time bars), f (free times), and c (conflict times).Default: %default""")

#### end of parms ####

parserOpts = {}


parserOpts['outline'] = [
    'begin_date', 'end_date', 'file', 'project', 'context', 'keyword', 'tag',
    'user', 'location', 'priority', 'search', 'omit', 'groupby', 'details', 'totalsfirst', 'values', 'vcal'
    ]

parserOpts['busy'] = [
    'begin_date', 'end_date', 'file', 'project', 'include', 'context', 'keyword',
    'tag', 'user', 'search', 'omit', 'opening', 'closing', 'minimum', 'wrap',
    ]

parserOpts['jump'] = [
    'begin_date'
    ]

def get_opts(repType, l=[]):
    parser = ETMOptParser(usage = '')
    for opt in parserOpts[repType]:
        s, a, dst, dflt, hlp = optionParms[opt]
        if dflt:
            parser.add_option(s, action = a, dest = dst, default = dflt,
                    help = hlp)
        else:
            parser.add_option(s, action = a, dest = dst,  help = hlp)
    if l:
        (opts, args) = parser.parse_args(l)
    else:
        (opts, args) = parser.parse_args()
    opts.__dict__['args'] = args
    return(opts.__dict__)

def keys2Nums(string):
    """
        Take a string in the format "((y,m,d),)" and convert it to a tuple
        of the form ((0,1,2),).
    """
    if type(string) == tuple:
        return(string)
    try:
        for key in fieldNum:
            string = re.sub(key, fieldNum[key], string)
        cols = eval(string)
        return(cols)
    except:
        return((0,1,2),)

nameHash = {
        u'o' : 'outline',
        u'b' : 'busy',
        u'v' : 'outline',
        u'j' : 'jump',
        }

message_log = None
if logging:
    messagelog = os.path.join(etmdir, 'message.log')
    #  clear the log
    message_log = open(messagelog, 'w')

def logmsg(mesg):
    if logging:
        message_log.write("%s\n" % str(mesg))

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:
            m = (m/6+1)*6
        return "%d.%dh" % (m/60, (m%60)/6)

def rel_monthday2date(sign, month, day):
    # print("in rel_... with", sign, month, day)
    today = datetime.datetime.today()
    themonth = today.month
    theyear = today.year
    relmonth = int(month)
    day = int(day)
    if sign == '-':
        relmonth = -relmonth
    themonth += relmonth
    if themonth < 1:
        themonth += 12
        theyear -= 1
    elif themonth > 12:
        themonth -= 12
        theyear += 1
    return(datetime.datetime(theyear, themonth, day))

def parse_date(date, hour=0, minute=0, zone = None):
    # return local time date adjusted for date and time in zone
    mesg = ''
    if zone == 'none':
        zone = None
    dt = None
    start_date = date
    now = datetime.datetime.now()
    if not date:
        date = now
    try:
        if type(date) in [str, unicode, int]:
            if date == 0:
                date = now
            else:
                m = rel_date_regex.match(date)
                if m:
                    if m.group(1) == '+':
                        date = now + int(m.group(2))*oneday
                    elif m.group(1) == '-':
                        date = now - int(m.group(2))*oneday
                else:
                    date = duparse('%s' % date)
        if type(date) == datetime.date:
            date = datetime.datetime.combine(date, datetime.time())
        dt = date.replace(hour = hour, minute = minute, second= 0, microsecond=0)
        if zone:
            dt = date.replace(tzinfo = gettz(zone))
            dt = dt.astimezone(gettz())
        dt = dt.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
        return(dt)
    except:
        mesg = "except: could not parse date=%s, hour=%s, minute=%s, zone=%s " % (
                date, hour, minute, zone)
        print mesg

        return(datetime.datetime.now())

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
    """
    st = s.strip()
    m = calc_days_regex.match(st)
    if m:
        # using days
        a = m.group(1).strip()
        v = m.group(2).strip()
        n = m.group(3).strip()
        days = int(n)
        try:
            a_datetime = parse_date(a)
        except:
            return((
                "error processing '%s': could not parse date '%s'" % (st, a), None))
        #  a_fmt = a_datetime.strftime("%Y-%m-%d")
        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), res))
    m = calc_date_regex.match(st)
    if m:
        # using date
        a = m.group(1).strip()
        v = m.group(2).strip()
        n = m.group(3).strip()
        # n must be a date
        try:
            a_datetime = parse_date(a)
        except:
            return((
                "error processing '%s': could not parse date '%s'" % (st, a), None))
        a_fmt = a_datetime.strftime("%Y-%m-%d")
        if v == '+':
            return((
                "error processing '%s': '+' can only be used with 'days'" %
                    (st), None))
        else:
            try:
                b_datetime = parse_date(n)
            except:
                return((
                    "error processing '%s': could not parse date '%s'" %
                        (st, n), None))
            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), "%s %s" % ( num_days, d)))
    return(("error parsing '%s'. (The '+' or '-' should have a space on each side.)" % st, None))


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 parse_opts(options):
    beg_dt, today, soondate = getToday()
    if 'begin_date' in options and options['begin_date']:
        if type(options['begin_date']) == tuple:
            beg_dt = duparse("%s-%s-%s" % options['begin_date'])
        else:
            m = rel_monthdate_regex.match(options['begin_date'])
            if m:
                beg_dt = rel_monthday2date(*m.groups())
            else:
                m = rel_date_regex.match(options['begin_date'])
                if m:
                    # we have a relative date in the form '+|- integer'.
                    if m.group(1) == '+':
                        beg_dt = datetime.datetime.today() + int(m.group(2)) * oneday
                    elif m.group(1) == '-':
                        beg_dt = datetime.datetime.today() - int(m.group(2)) * oneday
                else:
                    try:
                        beg_dt = duparse(options['begin_date'])
                    except:
                        print "could not parse date '%s'" % options['begin_date']
                        beg_dt = datetime.date.today()
            options['begin_date'] = tuple(map(int,
                beg_dt.strftime("%Y,%m,%d").split(',')))
    else:
        beg_dt = datetime.date.today()
        options['begin_date'] = tuple(map(int,
                beg_dt.strftime("%Y,%m,%d").split(',')))
    if 'end_date' in options and options['end_date']:
        if type(options['end_date']) != tuple:
            m = rel_monthdate_regex.match(options['end_date'])
            if m:
                end_dt = rel_monthday2date(*m.groups())
            else:
                m = rel_date_regex.match(options['end_date'])
                if m and m.group(1) == '+':
                    # we have a relative date in the form '+|- integer'.
                    beg_dt = duparse("%d-%02d-%02d" % options['begin_date'])
                    end_dt = beg_dt + int(m.group(2)) * oneday
                else:
                    end_dt = duparse(options['end_date'])
            options['end_date'] = tuple(map(int,
                end_dt.strftime("%Y,%m,%d").split(',')))
    else:
        end_dt = beg_dt + soon * oneday
        options['end_date'] = tuple(map(int,
            end_dt.strftime("%Y,%m,%d").split(',')))
    if 'cols' in options:
        if 'F' in options['cols']:
            # ignore other options
            options['cols'] = 'F'
        else:
            options['cols'] = keys2Nums(options['cols'])
            if type(options['cols']) == int:
                options['cols'] = (options['cols'],)
    else:
        options['cols'] = '((0,1,2),)'
    if 'details' in options:
        if options['details'] in [0, '0']:
            options['details'] = 0
        elif options['details'] in [1, '1']:
            options['details'] = 1
        elif options['details'] == '*':
            options['details'] = sort_str + u'I'
    else:
        options['details'] = 1
    return(options)

if __name__ == '__main__':
    now = datetime.datetime.now()
    pd = parse_date(now)
    print "now", pd
    pd = parse_date('+4')
    print "local parse_date('+4'):", pd
    pd = parse_date('8', zone="Pacific/Auckland")
    print "local parse_date('8') Pacific/Auckland:", pd
    print "duparse('2010-11-15')", duparse('2010-11-15')
    print "duparse('2:30p')", duparse('2:30p')
    s = "now  is   the time"
    print "normalize", s, normalize_whitespace(s)
