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 dateutil.tz import tzlocal, tzutc, gettz
from calendar import TextCalendar
from calendar import LocaleTextCalendar
from etm.etmVersion import version
from copy import deepcopy
from textwrap import wrap
from etm.etmRC import *
from platform import system

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)

etm_redirect = os.path.join(etmdir, 'etm.log')

logging = False
parser_msg = []
attrs = {}
all_off = ''

# 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_icalendar = False
try:
    from icalendar import Calendar, Event, Todo, Journal, FreeBusy, UTC
    has_icalendar = True
except:
    has_icalendar = False

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"

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

#### new color attr scheme ####
#### these attr are used to determine sort order as well as color
colors = {
    '0e'  : (0, 102, 0),
    '0r'  : (0, 102, 0),
    '1a'  : (102, 102, 0),
    '2tp' : (255, 51, 0),
    '3ti' : (204, 0, 153),
    '4ts' : (153, 0, 204),
    '5t'  : (0, 0, 204),
    '5tu' : (0, 0, 204),
    '7tw' : (160, 160, 255),
    '6tb' : (112, 112, 255),
    '8tf' : (153, 153, 153),
    '9n'  : (0, 153, 153),
    'l0'  : (255, 51, 0),
    'l1'  : (0, 102, 0),
    'l2'  : (204, 0, 153),
    'l3'  : (0, 0, 204),
    'd'   : (51, 51, 51),
}

rotate = {}
for r, t in [(rotate_action, action), 
        (rotate_event, event), 
        (rotate_done, done), 
        (rotate_note, note), 
        (rotate_reminder, reminder), 
        (rotate_task, task)]:
        rotate[t] = r

hexcolors = {}
for kn, val in colors.items():
    rpart,gpart,bpart = val
    hexcolors[kn] = "#%02X%02X%02X" % (rpart,gpart,bpart)

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 = {
            '0e'   : codes[event_color],
            '0r'   : codes[reminder_color],
            '1a'   : codes[action_color],
            '2tp'  : codes[task_pastdue_color],
            '3ti'  : codes[task_today_color],
            '4ts'  : codes[task_soon_color],
            '5t'   : codes[task_color],
            '5tu'  : codes[task_undated_color],
            '7tw'  : codes[task_waiting_color],
            '6tb'  : codes[task_beginby_color],
            '8tf'  : codes[task_finished_color],
            '9n'   : codes[note_color],
            'l0'   : codes[header_0_color],
            'l1'   : codes[header_1_color],
            'l2'   : codes[header_2_color],
            'l3'   : codes[header_3_color],
            'd'    : codes[detail_color],
        }
    else:
        all_off = ''
        attrs = {}
        for i in range(-2, 9):
            attrs[i] = ''
    return(all_off, attrs)

oneday = datetime.timedelta(days=1)
today = get_today()
newline_regex = re.compile(r'[\n\r]+', re.DOTALL|re.MULTILINE)
time_regex = re.compile(r'(\d+)\s*(\D*)$')
etime_regex = re.compile(r'\+((\d+)(:|h\s*))?((\d*)m?)?')
part_regex = re.compile(r'^(\S)\s*(\S.*)')
lineopts_regex = re.compile(r'-(?!\d)')
etmdata_regex = re.compile(r'%s/?' % etmdata)
tab_regex = re.compile(r'\t')
cdays_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')
historyleader = re.compile(r'^\w+\)\s*')
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})\!')
type_regex = re.compile(r'^\s*(\*|\&|\~|\!|\_+|\.+|#)')
view_regex = re.compile(r'^\s*(b|d|i|l|m)')
abbrv_regex = re.compile(r'^(\w+):(.+)$')
abbrv_entry = re.compile(r'.*\b(\w+)(\s*)$')
item_regex = re.compile(r'^\s*(\*|\&|\~|\+|\-|\!|\_+|\.+|\$)\s+(\S.*)$')
range_regex = re.compile(r'^(.*\b)(range\([\d\, ]+\))(.*)$')
project_regex = re.compile(r'\s*[^+\-\*\~@\s].*', re.DOTALL)
comment_regex = re.compile(r'\s*#')
finish_regex = re.compile(r'(@[^@]+)')
lastfield_regex = re.compile(r'.*[-@]([ckpz])\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?)')
# for relative date parsing
rel_date_regex = re.compile(r'^([-+])([0-9]+)')
# for the date calculator
calc_date_regex = re.compile(r'^(.+)\s+([-+])\s+(.+)(?!days?)')

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', 'end', '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'             : 'b',
    'context'           : 'c',
    'days'              : 'd',
    'end'               : 'e',
    'keyword'           : 'k',
    'user'              : 'u',
    'project'           : 'p',
    'omit'              : 'o',
    'vcal'              : 'v',
    'wrap'              : 'w',
    'search'            : 's',
    'file'              : 'f',
    'minimum'           : 'm',
    'minutes'           : 'H',
    'opening'           : 'O',
    'closing'           : 'C',
    'include_busy'      : 'i',
    'include_ledger'    : 'i',
    'include_item'      : 'i',
}

special_keys = [ 'chrcode', 'do_next', 'type', 'file', 'j', 't', 'description', 'id', 'details']
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']
reminder_keys = project_keys + ['s', 'd', 'a', 'A', 'n']
no_repeat_keys = ['d', 'b', 'c', 'k', 'n', 'f', 'U', 'z']
action_keys = ['c', 'd', 'g', 'p', 'k', 'n', 'j', 'U', 'z']
note_keys = ['c', 'd', 'k', 'n', 'j', 'U', 'z']
all_keys = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'i', 'k', 'l', 'M', 'm',
'n', 'o', 'p', 'j', 'r', 's', 'u', 'U', 'W', 'w', 'x', 'z']
sort_keys = [ 'd', 'b', 'p', 's', 'e', 'z', 'a', 'A', 'c', 'k', 'f', 'r', 'i',
'W', 'w', 'M', 'm', 'x', 'l', 'u', 'U', 'o', 'n', 'g',]

alphalist = ['a', 'A', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', '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 = ['i', 'l', 'm', 'M', 'o', 'r', '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'] = ['j', 'k'] + second_keys 
# groupby keyword:
g_keys['k'] = ['c', 'j'] + second_keys 
# groupby project
g_keys['p'] = ['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 = "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
    from urllib import urlopen, urlretrieve
    # strip the '-x' from experimental versions
    version = (version.split('-'))[0]
    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))
            return('A new version (%s) is available.' % (vstr))
        else:
            #  return('etm %s is the newest version available.' % 
            return('You are using the latest version.')

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

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

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

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

# license = "GNU General Public License (GPL)"
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.
"""

### 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 = [
'    zone         @z a timezone',
'    user         @U string',
'    context      @c string',
'    keyword      @k string',
'    goto         @g a list of related file paths and urls in the format',
'                    "long[|short]". Lists will display "short" if provided,',
'                    and otherwise "long". If selected in the gui, "long"',
'                    will be opened using the default application."',
'    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, ..., or [0-6] with 0=MO',
'                 The weekday can be further limited to a specific week in',
'                 the month, e.g. MO(-1) or TU(+3) as the last Monday or ',
'                 third Tuesday of the month. Lists can be used as well,',
'                 e.g. (MO, FR) or (MO(+2), MO(+4)).',
'      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')


task_text = [
'[._]+ task [Options]',
'Options:',
] + date_task_text + common_text + [
'      overdue    @o k)eep, r)estart, s)kip. Default: k',
'    note         @n string',
'    begin        @b an integer number of days before @d date',
'    finished     @f a date',
'    period       @p an integer number of minutes. Estimated/actual time',
'                    to complete task. Minutes for a finished task will be',
'                    reflected in time reckonings which include the date',
'                    the task was finished.',
]

project_text =['project [Options]:',
"    These options become the defaults for each of the project's items."] + common_text

event_text =['* event [Options]',
'Options:',
] + date_event_text + common_text + [
'    starttime    @s a time',
'    endtime      @e a time or a time addition string beginning with a',
'                 plus sign, e.g., +90, +1:30, +1h 30m, to be added to',
'                 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',
'    altcmd       @A an alternative command to run instead of the default',
'                 for alerts. Accepts the same substitution strings as',
'                 alertcmd - see the documentation in %s.' % etmrc,
'    note         @n string',
]

reminder_text =['& reminder [Options]',
'Options:',
] + date_event_text + common_text + [
'    starttime    @s a time or a list of times. Required.',
'    alerts       @a an integer or a list of integer minutes before',
'                 starttime. The default is 0.',
'    altcmd       @A an alternative command to run instead of the default',
'                 for alerts. Accepts the same substitution strings as',
'                 alertcmd - see the documentation in %s.' % etmrc,
'    note         @n string',
]

note_text =['! note [Options]',
'Options:',
] + [
"    date         @d a date. Required. The current date will be used if",
'                 one is not provided',
'    context      @c string',
'    keyword      @k string',
] 

action_text =['~ action [Options]',
'Options:',
'    date         @d a date. Required.',
'    zone         @z a timezone',
'    period       @p elapsed time in minutes. Required.',
'    context      @c string.',
'    keyword      @k string. A keyword containing one or more colons',
'                 e.g., "x:y", "x:z", will be split to form',
'                 groups and subgroups when aggregating times.',
]

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

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

Information:
    h       Show this help message
    c       Process 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. If the 
            current working directory contains a file named 'emtrc'
            then settings from that file will be used instead of those
            from '~/.etm/rc'.

Displays:
    b [B]   Display busy/free times using command line options [first
            prompting for options and displaying help information].
    d [D]   Display a list of all data items grouped by type, context,
            keyword and description using command line options [first
            prompting for options and displaying help information].
    i [I]   Display a list of events, actions and tasks using command line 
            options [first prompting for options and displaying help
            information].
    l [L]   Display a reckoning of time spent in events and actions 
            using command line options [first prompting for options and
            displaying help information].
""" % (version, copyright, sysinfo(), etminfo())

entry_help = """
<em>&lt;Esc&gt; cancels entry; &lt;Return&gt; accepts entry.<br>
Press F1 to select the type of item help displayed.<br>
Press F2 to display a twelve month calendar.<br>
Press TAB to select an insertion from a from a list of templates.<br>
Press Shift and Space after entering @c [word], @k [word] or @z [word] to complete [word] from a matching list of previously used c)ontexts, k)eywords or time z)ones.</em>
""" 

date_html="""\
<font size="+1"><b>date, time and list details</b></font>
<pre>
%s
</pre>
""" % "\n".join(list_text)

project_html="""
<title>project options</title>
<body text="%s" bgcolor="%s"><font size="+1"><b>The project line format</b></font>
<pre>
%s
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, "\n".join(project_text), entry_help)

action_html= """
<title>action options</title>
<body text="%s" bgcolor="%s"><font size="+1"><b>The format for action lines</b></font>
<pre>
%s
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, "\n".join(action_text), entry_help)

event_html="""
<title>event options</title>
<body text="%s" bgcolor="%s"><font size="+1"><b>The format for event lines</b></font>'
<pre>
%s
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, "\n".join(event_text), entry_help)

reminder_html="""
<title>reminder options</title>
<body text="%s" bgcolor="%s"><font size="+1"><b>The format for reminder lines</b></font>
<pre>
%s
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, "\n".join(reminder_text), entry_help)

note_html="""
<title>note options</title>
<body text="%s" bgcolor="%s"><font size="+1"><b>The format for note lines</b></font>
<pre>
%s
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, "\n".join(note_text), entry_help)

task_html="""
<title>task options</title>
<body text="%s" bgcolor="%s"><font size="+1"><b>The format for task lines</b></font>
<pre>
%s
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, "\n".join(task_text), entry_help)

help_text = """\
        <title>etm: event and task manager</title>
<center>
etm version %s. Copyright %s Daniel A Graham. All rights reserved.
<br>
%s
<br>
%s
</center>
<pre>
<b>General</b>
    F1:   Display this help message.
    F2:   Show information about etm.
    F3:   Check for an etm update.
    F4:   Display a twelve month calendar.
    F5:   Open the date calculator.
    F6:   Show local weather information.
    F7:   Show local sun and moon information.
    ^P:   Print the current report, help or calendar view.
    ^Q:   Quit etm.
    ^R:   Open rc file in external editor - etm must be restarted for
          changes to take effect.
    ^U:   Update data files (normally unnecessary).

<b>Main view</b>
    Display one week at a time with selected days highlighted in the
    calendar panel, relevant instances displayed in the list panel and busy
    times displayed as bars in the busy panel. 

    Details for an item highlighted (selected) in the list panel are
    displayed in the details bar immediately below the list panel.

    The status bar at the bottom of the main display is divided into four
    regions. From left to right:
        * The F1 help prompt. 
        * The time, if any, of the next alert for today.
        * The current display settings.
        * The status, if any, of the currently active action.

<b>Report views</b>
    <b>Busy view:</b> Busy and/or free times displayed graphically and/or
        textually.

    <b>Data view:</b> Display a list of all data items grouped by type,
       context, keyword and description using command line options.

    <b>Item view:</b> Similar to main view but for an arbitrary beginning
        and ending dates. Selected instances can be exported in either
        vCal/iCal or CSV format.

    <b>Ledger view:</b> An accounting of time spent in actions and/or
        events. The displayed report can be exported in CSV format.

<b>Item types</b>
    Each item requires a single line in a data file.
    <b>Action</b> 
        A record of the time-consuming action required to complete a
        task or participate in an event. Actions are not reminders, they
        are instead records of how time was actually spent. Action lines
        begin with '~'.
    <b>Event</b>
        Something that will happen on a particular day (or days) and,
        perhaps, at a particular time. Alerts are optional for events
        with starting times. Event lines begin with '*'.
    <b>Note</b> 
        A record of some useful information. Note lines begin with '!'.
    <b>Reminder</b>
        Similar to an event but without duration. A starting time is
        required and a list of starting times can be provided. If alerts
        are not specified, one will be triggered at each starting time.
        Reminder lines begin with '&'.
    <b>Task</b>
        A reminder of something that needs to be done. It may or may not
        have a due date. Task lines begin with one or more '.' or '_'

<b>Commands</b>
    <b>Creating or changing items</b>
        a:  begin a new action timer or toggle the timer for an active action.
        A:  If an action timer is running or paused, prompt for changes and
            record the action. Otherwise create a new action without using
            the timer.
        c:  begin a new item using a clone of the currently selected item.
        e:  create a new event.
        f:  mark the currently selected task finished.
        M:  move the currently selected item to a different project file.
        n:  create a new note.
        r:  create a new reminder.
        t:  create a new task.
        p:  open an existing project using 
                '%s' ('editor' in ~/.etm/rc)
            If an item is selected, its project will be opened. Otherwise, 
            a file selection dialog will be presented.
        P:  create a new project.
        u:  un-finish the currently selected task.
        Delete/Backspace:  delete the selected item. 

    <b>Movement</b>
        Left/Right arrow keys: move the selected days backward or forward
            by one day.
        Up/Down arrow keys: move the selected days backward or foreward by
            one week.
        Page Up/Down: move the selected day by backward or forward by one
            month.
        Shift Left/Right arrow keys (or , / .): move the selection in
            the list panel up or down by one item.
        Shift Up / Down arrow keys (or &lt; / &gt;): move the selection in
            the list panel up or down by five items.
        Spacebar: change the selected date to the current date.

        In the list panel:
            Click: highlight (select) an item. 
            Double-Click or Return: edit the selected item.

        In the busy panel:
            Click in a busy bar: highlight (select) the relevant item in
                the list panel.
            Double-Click: edit the relevant item.

    <b>Display</b>
        m: set main display options. Defaults for display options
            specified in <em>main_history</em> in <em>~/.etm/rc</em> and those used 
            during the current session can be selected from a drop-down
            list.
        0:  Restore the default display options: group by date, omit
            nothing.
      1-9:  Set display options using items 1 through 9 from    <em>main_history</em>.
        g:  if the currently selected item contains @g (goto) links, 
            then open a list of them for selection.
        o:  Show an outline of all project names, keywords and contexts.
        q:  Show the status of the background alert warnings queue for
            today.
       ^F:  Begin a case insensitive, regular expression search.
        b:  Set options and display a busy view. 
        d:  Set options and display a data view.  
        i:  Set options and display an item view.  
        l:  Set options and display a ledger view. 
</pre>
""" % (version, copyright, etminfo(), sysinfo(), editor)

view_help = """
<em>&lt;Esc&gt; cancels entry; &lt;Return&gt; accepts entry.<br>
Press F2 to display a twelve month calendar.<br>
Press TAB after [options] to complete [options] from the current history list.<br>
Press Shift and Space after entering -c [word] or -k [word] to complete [word] from a matching list of previously used c)ontexts or k)eywords.</em>
"""

mainview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s"> 
<font size="+1"><b>Main view options</b></font>
<pre>
  -b BEGIN    Date. Display items for seven days beginning with this 
              (fuzzy parsed) date.
  -g GROUPBY  An element from [d,p,c,k] where:
                  d: group by date
                  p: group by project
                  c: group by context
                  k: group by keyword
  -c CONTEXT  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.
  -f FILE     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.
  -k KEYWORD  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.
  -u USER     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.
  -p PROJECT  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.
  -s SEARCH   Regular expression. Include items containing FIND (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.
  -o OMIT     String with characters from the following: 
                  a: actions 
                  b: task begin dates 
                  e: events
                  f: finished tasks 
                  n: notes
                  r: reminders
                  t: all tasks 
                  u: undated tasks 
                  w: waiting tasks 
              If OMIT begins with '!', then only show items with types 
              belonging to OMIT. Otherwise only show items with types 
              NOT belonging to OMIT.
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, view_help)

itemview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s">
<font size="+1">Item view options</font>
<pre>
  -b BEGIN    Date. Display items beginning with this (fuzzy parsed) date.
  -e END      Date. Display items beginning with BEGIN and ending with this
              (fuzzy parsed) date.
  -d DAYS     Positive integer. Display items ending DAYS days after BEGIN
              (or beginning DAYS days before END). It is an error to specify
              more than two of BEGIN, DAYS and END.
  -g GROUPBY  An element from [d,p,c,k] where:
                  d: group by date
                  p: group by project
                  c: group by context
                  k: group by keyword
                  Default: d.
  -o OMIT     String with characters from the following: 
                  a: actions 
                  b: begin task dates 
                  e: events
                  f: finished tasks 
                  n: notes
                  r: reminders
                  t: all tasks 
                  u: undated tasks 
                  w: waiting tasks 
              If OMIT begins with '!', then only show items with types 
              belonging to OMIT. Otherwise only show items with types 
              not belonging to OMIT.
  -c context  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.
  -f file     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.
  -i include  string containing characters for fields to include in a 
              second details line. e.g. -i ckp would display, in this 
              order, c)ontext, k)eywords and p)roject. default: ''.
  -k keyword  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.
  -u USER     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.
  -p project  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.
  -s search   regular expression. include items containing find (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.
  -v VALUES   Export items in CSV (comma separated values) format to
              VALUES.csv in %s.
  -x EXPORT   Export list view items in vCal format to file EXPORT.ics
              in %s.
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, export, export, view_help)

dataview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s">
<font size="+1">Data view options</font>
<pre>
  -o OMIT     String with characters from the following: 
                  a: actions 
                  b: begin task dates 
                  e: events
                  f: finished tasks 
                  n: notes
                  r: reminders
                  t: all tasks 
                  u: undated tasks 
                  w: waiting tasks 
              If OMIT begins with '!', then only show items with types 
              belonging to OMIT. Otherwise only show items with types 
              not belonging to OMIT.
  -c CONTEXT  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.
  -f FILE     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.
  -k KEYWORD  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.
  -u USER     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.
  -p PROJECT  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.
  -s SEARCH   Regular expression. Include items containing FIND (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.
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, view_help)

busyview_html = """
<body text="%s" bgcolor="%s">
<font size="+1">Busy view options</font>
<pre>
  -b BEGIN    Date. Display items beginning with this (fuzzy parsed) date.
  -e END      Date. Display items beginning with BEGIN and ending with this
              (fuzzy parsed) date.
  -d DAYS     Positive integer. Display items ending DAYS days after
              BEGIN (or beginning DAYS days before END). It is an
              error to specify more than two of BEGIN, DAYS and END.
  -c CONTEXT  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.
  -f FILE     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.
  -k KEYWORD  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.
  -u USER     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.
  -p PROJECT  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.
  -s SEARCH   Regular expression. Include items containing FIND (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.
  -i INCLUDE  Four digit string of the form bBfF where b, B, f and F are
              either 0 or 1. Include busy time bars if b = 1; include busy
              times if B = 1; include free time bars if f = 1; include free
              times if F = 1. Default: %s.
  -w WRAP     Positive integer. Skip WRAP minutes before and after busy
              periods when computing free periods. Default: %s.
  -m MINIMUM  Positive integer. The minimum length in minutes for an
              unscheduled period to be displayed. Default: %s.
  -O OPENING  Time. The opening or earliest time (fuzzy parsed) to be
              considered when displaying unscheduled periods. Default: %s.
  -C CLOSING  Time. The closing or latest time (fuzzy parsed) to be
              considered when displaying unscheduled periods. Default: %s.
  -M MINUTES  Integer. The number of minutes covered by one character
              in the time bar. It must be the case that 60 %% MINUTES
              = 0. Recommended choices are 10, 12 or 15. Default: %s.
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, busy_include, wrap, minimum, opening, closing, charMinute, view_help)

ledgerview_html = """
<title>etm</title>
<body text="%s" bgcolor="%s">
<font size="+1">Ledger view options</font>
<pre>
  -b BEGIN    Date. Include items beginning with this (fuzzy parsed)
              date.
  -e END      Date. Include items beginning with BEGIN and ending with
              this (fuzzy parsed) date.
  -d DAYS     Positive integer. Include items ending DAYS days after
              BEGIN (or beginning DAYS days before END). It is an
              error to specify more than two of BEGIN, DAYS and END.
  -i INCLUDE  Four digit string of the form CDKI where C, D, K and I are
              integers. Subtotal by category if C is positive and in an
              order corresponding to C. Subtotal by date if D is positive
              and in an order corresponding to D. Subtotal by keywords to
              a depth corresponding to the value of K if positive. E.g., 
              if K is 2, and there is an item with keyword 'a:b:c', then
              subtotals corresponding to 'a' and 'a:b' would be formed.
  -c CONTEXT  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.
  -f FILE     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.
  -k KEYWORD  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.
  -u USER     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.
  -p PROJECT  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.
  -s SEARCH   Regular expression. Include items containing FIND (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.
  -v VALUES   Export report in CSV (comma separated values) format to
              VALUES.csv in %s.
</pre>
%s
</body>
""" % (main_fgcolor, main_bgcolor, export, view_help)


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


bhelp =  """Date. Display items beginning with this date (fuzzy parsed) and continuing for the next DAYS days. Default: today.
"""

dhelp = """Positive integer. The number of days beyond BEGIN to display. Default: 6.
"""

ehelp = """Date. Display items beginning with BEGIN and ending with this date (fuzzy parsed). Default: BEGIN plus DAYS days.
"""

### mparser ###
mparser = ETMOptParser(usage = '')
mparser.add_option("-b",  dest="begin",
    action="store",
    help = bhelp)
mparser.add_option("-g", action = "store",
    dest='groupby',  choices = ['d', 'p', 'c', 'k'],
    help = """An element from [d,p,c,k] where:	                      \n
    d: group by date                        						  \n
    p: group by project				                				  \n
    c: group by context								                  \n
    k: group by keyword								                  \n
    Default: d.""")
mparser.add_option("-c", action = "store",
    dest='context',
    help = """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.""")
mparser.add_option("-f", action = "store",
    dest='file',
    help = """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.""")
mparser.add_option("-k", action = "store",
    dest='keyword',
    help = """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.""")
mparser.add_option("-p", action = "store",
    dest='project',
    help = """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.""")
mparser.add_option("-u", action = "store",
    dest='user',
    help = """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.""")
mparser.add_option("-s", 
    action="store",
    dest='search',
    help = """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.""")
mparser.add_option("-o", action = "store",
    dest='omit', default='',
    help = """String. Show/hide a)ctions, task b)egin dates, 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', 'e', 'f', 'n', 'r', 't', 'u' and/or 'w' and begins with '!' (show) or does not being with '!' (hide). Default: %default.""")

### iparser ###
iparser = ETMOptParser(usage = '')
iparser.add_option("-b",  dest="begin",
    action="store",
    help = bhelp)
iparser.add_option("-d", action = "store",
    dest='days', type = int,
    help = dhelp)
iparser.add_option("-e", action = "store",
    dest='end',
    help = ehelp)
iparser.add_option("-g", action = "store",
    dest='groupby', choices = ['d', 'p', 'c', 'k'],
    help = """An element from [d,p,c,k] where:	                      \n
    d: group by date                        						  \n
    p: group by project				                				  \n
    c: group by context								                  \n
    k: group by keyword								                  \n
    Default: d.""")
iparser.add_option("-c", action = "store",
    dest='context',
    help = """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.""")
iparser.add_option("-f", action = "store",
    dest='file',
    help = """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.""")
iparser.add_option("-i", action = "store",
    dest='include_item',
    help = """String. For each item, display a second 'details' line which includes fields corresponding to characters in INCLUDE. E.g., '-i en' would display e)nding time and n)ote in the details line.""")
iparser.add_option("-k", action = "store",
    dest='keyword',
    help = """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.""")
iparser.add_option("-u", action = "store",
    dest='user',
    help = """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.""")
iparser.add_option("-p", action = "store",
    dest='project',
    help = """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.""")
iparser.add_option("-s", 
    action="store",
    dest='search',
    help = """Regular expression. Include items containing FIND (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.""")
iparser.add_option("-o", action = "store",
    dest='omit', 
    help = """String. Show/hide a)ctions, task b)egin dates, 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', 'e', 'f', 'n', 'r', 't', 'u' and/or 'w' and begins with '!' (show) or does not being with '!' (hide). Default: %default.""")
iparser.add_option("-v", action = "store",
    dest='values',
    help = """Export items in CSV (comma separated values) format to VALUES.csv in %s.""" % export)
iparser.add_option("-x", action = "store",
    dest='export',
    help = """Export items in iCal/vCal format to file EXPORT.ics in %s.""" % export)

### bparser ###
bparser = ETMOptParser(usage = '')
bparser.add_option("-b",  dest="begin",
    action="store",
    help = bhelp )
bparser.add_option("-d", action = "store",
    dest='days', type = int, 
    help = dhelp)
bparser.add_option("-e", action = "store",
    dest='end',
    help = ehelp)
bparser.add_option("-c", action = "store",
    dest='context',
    help = """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.""")
bparser.add_option("-f", action = "store",
    dest='file',
    help = """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.""")
bparser.add_option("-k", action = "store",
    dest='keyword',
    help = """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.""")
bparser.add_option("-u", action = "store",
    dest='user',
    help = """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.""")
bparser.add_option("-p", action = "store",
    dest='project',
    help = """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.""")
bparser.add_option("-s", 
    action="store",
    dest='search',
    help = """Regular expression. Include items containing FIND (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.""")
bparser.add_option("-w", action = "store",
    dest='wrap',  type = int, default=wrap,
    help = """Positive integer. Provide a buffer of WRAP 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("-M", action = "store",
    dest='minutes',  type = int, default=charMinute,
    help = """Positive integer. The number of minutes covered by a single character in a time bar. It must be the case that 60 % MINUTES = 0. Recommended choices are 10, 12 or 15 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_busy', type=str, default = busy_include,
    help = """Four digit string of the form bBfF where b, B, f and F are either 0 or 1. Include busy time bars if b = 1; include busy times if B = 1; include free time bars if f = 1; include free times if F = 1. Default: %default""")

### lparser ###
lparser = ETMOptParser(usage = '')
lparser.add_option("-b",  dest="begin",
    action="store",
    help = bhelp )
lparser.add_option("-d", action = "store",
    dest='days', type = int,
    help = dhelp)
lparser.add_option("-e", action = "store",
    dest='end',
    help = ehelp)
lparser.add_option("-c", action = "store",
    dest='context',
    help = """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.""")
lparser.add_option("-f", action = "store",
    dest='file',
    help = """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.""")
lparser.add_option("-k", action = "store",
    dest='keyword',
    help = """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.""")
lparser.add_option("-u", action = "store",
    dest='user',
    help = """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.""")
lparser.add_option("-p", action = "store",
    dest='project',
    help = """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.""")
lparser.add_option("-s", 
    action="store",
    dest='search',
    help = """Regular expression. Include items containing FIND (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.""")
lparser.add_option("-i", action = "store",
    dest='include_ledger', type=str, default = '2120',
    help = """Four digit string of the form CDKI where C, D, K and I are integers. Subtotal by category if C is positive and in an order corresponding to C. Subtotal by date if D is positive and in an order corresponding to D. Subtotal by keywords to a depth corresponding to the value of K if positive. E.g., if K is 2, and there is an item with keyword \'a:b:c\', then subtotals corresponding to \'a\' and \'a:b\' would be formed. Subtotal by item if I is positive. Default: %default""")
lparser.add_option("-o", action = "store",
    dest='omit', 
    help = """String. Show/hide a)ctions, e)vents and/or t)tasks depending upon whether OMIT contains 'a', 'e' and/or 't' and begins with '!' (show) or does not being with '!' (hide). Default: %default.""")
lparser.add_option("-v", action = "store",
    dest='values',
    help = """Export report in CSV (comma separated values) format to VALUES.csv in %s.""" % export)

### dparser ###
dparser = ETMOptParser(usage = '')
dparser.add_option("-c", action = "store",
    dest='context',
    help = """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.""")
dparser.add_option("-f", action = "store",
    dest='file',
    help = """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.""")
dparser.add_option("-k", action = "store",
    dest='keyword',
    help = """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.""")
dparser.add_option("-u", action = "store",
    dest='user',
    help = """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.""")
dparser.add_option("-o", action = "store",
    dest='omit', default = '',
    help = """String. Show/hide a)ctions, task b)egin dates, 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', 'e', 'f', 'n', 'r', 't', 'u' and/or 'w' and begins with '!' (show) or does not being with '!' (hide). Default: %default.""")
dparser.add_option("-p", action = "store",
    dest='project',
    help = """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.""")
dparser.add_option("-s", 
    action="store",
    dest='search',
    help = """Regular expression. Include items containing FIND (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.""")

parserHash = {
        'm' : mparser,
        'i' : iparser,
        'b' : bparser,
        'l' : lparser,
        'd' : dparser,
        }

nameHash = {
        'm' : 'main options',
        'i' : 'item options',
        'b' : 'busy options',
        'l' : 'ledger options',
        'd' : 'data options',
        }

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 parse(str):
    if type(str) == datetime.datetime:
        return str
    elif type(str) == datetime.date:
        return datetime.datetime.combine(str, datetime.time())
    else:
        try:
            return duparse(str, dayfirst=False, yearfirst=False, fuzzy=True) 
        except:
            print 'exception parse(%s)' % str, type(str)
            return()


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

def l2u(l_date, l_time=None):
    """Make sure that the dst flag is -1 -- this tells mktime to take daylight savings into account"""
    rgx = re.compile(r'\s*\+\d+:\d\d\s+\((.*)\)')
    if type(l_date) == str:
        l_date = duparse("%s" % (l_date))
    if l_time == None:
        l_dto = datetime.datetime.combine(l_date, datetime.time()) 
    elif type(l_time) == str:
        m = rgx.match(l_time)
        if m:
            l_time = m.group(1)
        try:
            l_dto = duparse("%s %s" % (l_date.strftime(date_fmt), l_time))
        except:
            print "except: l2u exception", l_date, l_time
            l_dto = datetime.datetime.combine(l_date, datetime.time()) 
    else:
        l_dto = datetime.datetime.combine(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.datetime(u.tm_year, u.tm_mon, u.tm_mday, u.tm_hour, u.tm_min, u.tm_sec,
            tzinfo=UTC)

def midnight(d=None):
    if d:
        dt = parse(d).replace(hour=0,minute=0,second=0,microsecond=0)
    else:
        dt = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
    return(dt)

def has(hash, key):
    "Return true if key and hash and key in hash and hash[key]"
    try:
        return(key and hash and key in hash and 
            hash[key] not in [None, '', {}])
    except:
        print 'except', key, hash
        return(False)

def cal(advance=0):
    c = LocaleTextCalendar(week_begin, '')
    cal = []
    y = int(today.strftime("%Y"))
    m = 1
    if advance != 0:
        y += advance
    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(gui_encoding))
    return s

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

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
        #  print "parse_date", date, hour, minute, zone
    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 parse_datetime(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_tmp = date.replace(hour = hour, minute = minute, second=0, microsecond=0)
        if zone:
            dt_tmp = dt_tmp.replace(tzinfo = gettz(zone))
            dt_tmp = dt_tmp.astimezone(gettz())
        dtime = dt = dt_tmp.replace(tzinfo=None)
        d = dt.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
        return(d, dtime)
    except:
        mesg = "except: could not parse date=%s, hour=%s, minute=%s, zone=%s " % (
                date, hour, minute, zone)
        print mesg
        ndt = now.replace(tzinfo = None)
        nd = ndt.replace(hour=0, minute=0, second=0, microsecond=0)
        return(nd, ndt)

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 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:
            print('exception in sort_listofhashes_by_field')
            print('the problem hash:')
            print(listofhashes[i])
            print('thefields:')
            print(thefields)
            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  and prompt for options."
    global parser_msg
    cmdlc = cmd.lower()
    get_help(cmdlc) 
    print("\n".join(parser_msg))
    argstr = raw_input('%s: ' % nameHash[cmdlc])
    if argstr:
        args = argstr.split(' ')
    else:
        args = []
    return(args)

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

def clear_msg():
    global parser_msg
    parser_msg = []

def parse_opts(line):
    shash = {}
    # parts = line.split(' -')
    parts = lineopts_regex.split(line)
    t = str(parts.pop(0).strip())
    if t not in ['m', 'i', 'b', 'l', 'd']:
        print "error"
        return({}, ["%s not in ['m', 'i', 'b', 'l']" % t], '')
    args = []
    slst = []
    if len(parts) > 0:
        for part in parts:
            m = part_regex.match(part)
            if m:
                k = str(m.group(1))
                v = str(m.group(2))
                args.extend(['-%s' % k.strip(), v.strip()])
    dflts, errors = parse_args(t, [])
    shash, errors = parse_args(t, args)
    rhash = {}
    lst = []
    if not errors: 
        for key in opt_keys[t]:
            if has(shash, key) and shash[key] and (not has(dflts, key) or dflts[key] != shash[key]):
                rhash[key] = shash[key]
                lst.append('-%s %s' % (short_opts[key], shash[key]))
                if type(shash[key]) == datetime.datetime:
                    slst.append('-%s %s' % (short_opts[key], shash[key].strftime(date_fmt)))
                else:
                    slst.append('-%s %s' % (short_opts[key], shash[key]))

    s = " ".join(slst)
    if t.lower() == 'm':
        return(rhash, errors, s)
    else:
        return(shash, errors, s)

def get_mainopts(hash):
    slst = []
    for key in opt_keys['m']:
        if has(hash, key):
            if type(hash[key]) == datetime.datetime:
                slst.append('-%s %s' % (short_opts[key], hash[key].strftime(date_fmt)))
            else:
                slst.append('-%s %s' % (short_opts[key], hash[key]))
    if slst:
        s = "display: %s" % " ".join(slst)
    else:
        s = ''
    return(s)

def parse_args(cmd, args=[]):
    """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()
    global opening, closing, minutes
    options = None
    errors = []
    mesg = []

    if cmdlc in ['m', 'b', 'd', 'i', 'l']:
        if cmduc == cmd:
            args = get_args(cmdlc) 
        try:
            options = {}
            (def_opts, toss1) = parserHash[cmdlc].parse_args([])
            try:
                (options, toss2) = parserHash[cmdlc].parse_args(args)
            except:
                errors = ["could not parse %s using parserHash['%s']" % 
                        (args, cmdlc)]
                return({}, errors)

            options.view = cmdlc

            new_days = has(options.__dict__, 'days') and \
                    type(options.__dict__['days']) == int

            if new_days:
                if options.days < 0:
                    errors.append('option -d: invalid value (%s) for DAYS' 
                            % options.days)
                else:
                     days = options.days 
            else:
                days = 6

            new_begin = has(options.__dict__, 'begin') and \
                    type(options.__dict__['begin']) == str
            if new_begin:
                options.begin = parse_date(options.begin)
            else:
                options.begin = midnight()

            new_end = has(options.__dict__, 'end') and \
                type(options.__dict__['end']) == str
            if new_end:
                options.end = parse_date(options.end)
            else:
                options.end = options.begin + days*oneday

            if cmdlc == 'm':
                new_days = False
                new_end = False
            else: 
                if 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 and not new_end:
                    options.end = options.begin + days*oneday
                elif new_end and not new_begin:
                    options.begin = options.end - days*oneday
                if new_begin or new_end:
                    options.end = parse_date(options.end)
                    options.begin = parse_date(options.begin)
                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, mesg = format_date(opening, timefmt)
                if mesg:
                    errors.append(mesg)
                options.closing, mesg = format_date(closing, timefmt)
                if mesg:
                    errors.append(mesg)
                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('-S'):
                tmp = int(options.minutes)
                if 60 % tmp == 0:
                    minutes = tmp

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

    return(options.__dict__, errors)

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 "parse('2010-11-15')", parse('2010-11-15')
    print "parse('2:30p')", parse('2:30p')
