#!/usr/bin/env python3

import optparse,os,re,sys,time

# Components of the current date and time and TZ details are needed in lots of
# places in this code, so I'm just making them globally available. So there.
YYYY,MM,DD,hh,mm,ss,dummy,dummy,DST=time.localtime()
defaults=dict(year=YYYY,mon=MM,day=DD,hour=hh,min=mm,sec=ss)
tz_name=time.tzname[DST]
tz_offset=int(time.mktime(time.localtime(86400))-time.mktime(time.gmtime(86400)))

# Our default time input format is also used several times, and it's
# silly (and error-prone) to repeat it in all those places.
IN_FMT='%Y-%m-%d %H:%M:%S %Z'

# Get names of months in our current locale.
month_names=[time.strftime('%B',(2017,m,1,12,0,0,0,0,0)).lower() for m in range(1,13)]
month_abbr=[time.strftime('%b',(2017,m,1,12,0,0,0,0,0)).lower() for m in range(1,13)]

def month_num(s):
  "Return the number (1-12) of the named month. Return 0 if uncertain."

  s=s.lower()

  # Try month abbreviations first, then full names.
  for a in (month_abbr,month_names):
    # Find all plausible month name matches.
    p=[i+1 for i in range(len(a)) if a[i].lower().startswith(s)]
    if len(p)==1:
      # There was exactly one match, so return the number of that month.
      return p[0]
  return 0

def timeFromInteger(td):
  'Given a time expressed as a single value, return a time.struct_time for it.'

  t=td.sec
  if   td.units=='w':t*=7*86400
  elif td.units=='d':t*=86400
  elif td.units=='h':t*=3600
  elif td.units=='m':t*=60
  return time.localtime(t)

# This big structure identifies, validates, and parses lots of different
# string expressions of dates and times down to a Unix Epoch time value.
#
# Each supported format has an english description, an RE that must match the
# input string, and one or more validate/parse function pairs that take
# RE-parsed components and validate the values there and actually parse those
# values down to a Unix Epoch time.
#
# The structure below is a tuple of such formats. The first entry to
# successfully match, validate, and parse wins.
#
# The RE matching, if successful, produces a typeless object whose attributes
# are named according to the named groups in the RE and whose values are the
# corresponding values from the matched string. Any such values that can be
# converted to integer values are converted.
#
# Assuming an RE match is found, each validate/parse function for the matched
# time format is tried. Both functions take the single argument returned from
# matching the RE. The validate functions True iff the values found are
# agreeable. The parse function returns a Unix Epoch time from those values.

tf=(
  ( # Handle some single number of seconds, minutes, hours, days, or weeks since
    # the start of the C Epoch.
    # A single integer value followed by
    '"N[smhdw]" where N is an integer optionally followed by a unit character.',
    re.compile(r'^(?P<sec>-?\d+)(?P<units>[smhdw])?$'),
    (
      (
        (lambda _: True), # The matched RE is all the vetting we need.
        timeFromInteger
      ),
    )
  ),
  ( # Handle hour:minutes:second expressions.
    '"Hour{:.}Minute{:.}Second" where the separators must match.',
    re.compile(r'^(?P<hour>-?\d+)(?P<tsep>[:.])(?P<min>-?\d+)(?P=tsep)(?P<sec>-?\d+)$'),
    (
      (
        (lambda td: td.hour>-24 and td.hour<24 and td.min>-60 and td.min<60 and td.sec>-60 and td.min<60),
        #(lambda td: time.strptime('%d-%d-%d %d:%d:%d %s'%(YYYY,MM,DD,td.hour,td.min,td.sec,tz_name),IN_FMT))
        (lambda td: time.localtime(time.mktime((YYYY,MM,DD,td.hour,td.min,td.sec,0,0,DST))))
      ),
    )
  ),
  ( # "Mon dd hour:minute:second"
    '"MonthName Day Hour{:.}Minute{:.}Second" where the month may be abbreviated.',
    re.compile(r'^(?P<mname>\w+)\s+(?P<day>\d+)(\s+(?P<hour>-?\d+)(?P<tsep>[:.])(?P<min>-?\d+)((?P=tsep)(?P<sec>-?\d+))?)?$',re.IGNORECASE),
    (
      (
        (lambda td: month_num(td.mname)!=0 and td.day>-32 and td.day<32 and td.hour>-24 and td.hour<24 and td.min>-60 and td.min<60 and td.sec>-60 and td.min<60),
        (lambda td: time.strptime('%d %d %d %d:%d:%d'%(YYYY,month_num(td.mname),td.day,td.hour,td.min,td.sec),'%Y %m %d %H:%M:%S'))
      ),
    )
  ),
  ( # "YYYY-mm-dd hour:minute:second"
    '"YYYY{-/}MM{-/}{DD} Hour{:.}Minute{:.}Second".',
    #re.compile(r'^(?P<year>\d{4})(?P<dsep>[-/])(?P<month>\d{1,2})(?P=dsep)(?P<day>\d{1,2})\s+(?P<hour>-?\d+)(?P<tsep>[:.])(?P<min>-?\d+)(?P=tsep)(?P<sec>-?\d+)$',re.IGNORECASE),
    re.compile(r'^(?P<year>\d{4})(?P<dsep>[-/])(?P<month>\d{1,2})(?P=dsep)(?P<day>\d{1,2})(\s+(?P<hour>-?\d+)(?P<tsep>[:.])(?P<min>-?\d+)((?P=tsep)(?P<sec>-?\d+))?)?$',re.IGNORECASE),
    (
      (
        (lambda td: td.month>=-12 and td.month<=12 and td.day>-32 and td.day<32 and td.hour>-24 and td.hour<24 and td.min>-60 and td.min<60 and td.sec>-60 and td.min<60),
        (lambda td: time.strptime('%d-%d-%d %d:%d:%d'%(td.year,td.month,td.day,td.hour,td.min,td.sec),'%Y-%m-%d %H:%M:%S'))
      ),
    )
  ),
)

op=optparse.OptionParser(
  add_help_option=False,
  prog=os.path.basename(sys.argv[0]),
  usage='''Usage: %%prog [OPTIONS] [t]

Description:
  Output the current or given (t) time in the specified or default format.
  A number of input formats are recognized:

%s

Output formatting (from Python's docs for its time module):
  %%a  Locale's abbreviated weekday name.   
  %%A  Locale's full weekday name.  
  %%b  Locale's abbreviated month name.     
  %%B  Locale's full month name.    
  %%c  Locale's appropriate date and time representation.   
  %%d  Day of the month as a decimal number [01,31].    
  %%H  Hour (24-hour clock) as a decimal number [00,23].        
  %%I  Hour (12-hour clock) as a decimal number [01,12].        
  %%j  Day of the year as a decimal number [001,366].   
  %%m  Month as a decimal number [01,12].       
  %%M  Minute as a decimal number [00,59].      
  %%p  Locale's equivalent of either AM or PM. (1)
  %%S  Second as a decimal number [00,61]. (2)
  %%U  Week number of the year (Sunday as the first day of the week) as a decimal
      number [00,53]. All days in a new year preceding the first Sunday are
      considered to be in week 0. (3)
  %%w  Weekday as a decimal number [0(Sunday),6].       
  %%W  Week number of the year (Monday as the first day of the week) as a decimal
      number [00,53]. All days in a new year preceding the first Monday are
      considered to be in week 0. (3)
  %%x  Locale's appropriate date representation.    
  %%X  Locale's appropriate time representation.    
  %%y  Year without century as a decimal number [00,99].        
  %%Y  Year with century as a decimal number.   
  %%Z  Time zone name (no characters if no time zone exists).   
  %%%%  A literal '%%' character.

  1. When used with the strptime() function, the %%p directive only affects the
     output hour field if the %%I directive is used to parse the hour.
  2. The range really is 0 to 61; this accounts for leap seconds and the (very
     rare) double leap seconds.
  3. When used with the strptime() function, %%U and %%W are only used in
     calculations when the day of the week and the year are specified.
'''%'\n\n'.join(['    '+descr for descr,re,fns in tf]))
op.add_option('-c','--epoch',dest='epoch',default=False,action='store_true',help="Output the number of C Epoch seconds. Overrides --output-format.")
op.add_option('-h','--help',dest='help_me',default=False,action='store_true',help="Display this help text and exit.")
#op.add_option('-t','--time',dest='time',action='store',default=None,help="Use the given time rather than the current time.")
#op.add_option('--input-format',dest='ifmt',action='store',default='%x %X',help="Use the given format to read -t's argument.")
op.add_option('--output-format',dest='ofmt',action='store',default=IN_FMT,help="Use the given format to write the given or default time. Overridden by -c (--epoch).")
op.add_option('--utc-output',dest='utc_output',action='store_true',help="Use UTC for any date/time output.")
op.add_option('--age',dest='age',action='store_true',help="Report how far in the past (or future, with a leading - on the output) the current (useless) or given (more interesting) time is.")

opt,args=op.parse_args()
if opt.help_me:
  op.print_help()
  sys.exit(0)

now=int(time.time())

if args:
  ts=args[0]  # Get the time string from the argument list.

  # Match one of our timestamp input formats.
  tstruct=None
  for desc,pat,functions in tf:
    #print 'D: trying %r'%(pat.pattern,)
    m=pat.search(ts)
    if m:
      #print 'D: RE matched!'
      # Parse time component by RE, converting strings to integers where possible.
      td=type('',(),m.groupdict(''))
      for var,val in list(td.__dict__.items()):
        if var[0]!='_':
          if var in defaults and val in (None,''):
            val=defaults[var]
            setattr(td,var,val)
          try:
            setattr(td,var,int(val))
          except:
            pass
      #print 'D: %s'%(', '.join(['%s=%r'%(var,val) for var,val in td.__dict__.items() if var[0]!='_']))
      # Figure out what sort of time expression this is.
      for validate,parse in functions:
        if validate(td):
          tstruct=parse(td)
          break
      if tstruct!=None:
        break
  if tstruct==None:
    print('Unrecognized time format in argument: %r'%ts, file=sys.stderr)
    sys.exit(1)
  # Convert our parset time.time_struct object to a Unix Epoch time value.
  t=int(time.mktime(tstruct))

  #print ' '.join(['%s=%r'%(p,td.__dict__.get(p)) for p in 'd1 d2 d3 dsep hour min sec tsep units'.split()])
else:
  # No time argument found on command line, so use current time.
  t=now

# Output time t in the requested or default format.
if opt.age:
  output=''
  diff=now-t
  if diff<0:
    ouput='-'
    diff=-diff
  d,s=divmod(diff,86400) # Get days and seconds of this age
  if diff>86400:
    output+='%d days '%d
  if s:
    output+='%02d:%02d:%02d'%(s/3600,(s%3600)/60,s%60)
  if output:
    print(output)
  else:
    print(0)
elif opt.epoch:
  print(t)
elif opt.utc_output:
  print(time.strftime(opt.ofmt,time.gmtime(t)))
else:
  print(time.strftime(opt.ofmt,time.localtime(t)))
