#!/usr/bin/env python
# Copyright (C) 2011-2013 by Yu-Jie Lin
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


from __future__ import print_function
import argparse
import math
import re
import sys
import time

import urwid


__program__ = 'urtimer'
__version__ = '0.5.1'
__license__ = 'MIT License'
__description__ = 'Simple countdown timer and stopwatch using urwid'
__website__ = 'https://bitbucket.org/livibetter/urtimer'

__author__ = 'Yu-Jie Lin'
__email__ = 'livibetter@gmail.com'


class UserInterrupt(Exception):

  pass


class TimerWidget(urwid.BigText):

  _selectable = True
  _modes = ('countdown', 'stopwatch')
  signals = ['started', 'ended']

  def __init__(self, t=60*5, font=None, no_ds=False, no_title=False,
               mode=_modes[0]):

    self.started = False
    self.mode = mode
    self.t = t if mode == 'countdown' else 0
    self.no_ds = no_ds
    self.no_title = no_title
    self.set_text(self.format_text(t))
    if not font:
      self.font = urwid.HalfBlock6x5Font()
    self.__super.__init__(self.get_text()[0], self.font)

  def to_hms(self, ss):

    hh = ss // 3600
    ss = ss - hh * 3600
    mm = ss // 60
    # truncate to first digit after decimal
    ss = (ss - mm * 60) * 10 / 10.0
    return hh, mm, ss

  def format_text(self, ss):

    fmt = '%02d:%02d:%02d' if self.no_ds else '%02d:%02d:%04.1f'
    return fmt % self.to_hms(ss)

  @property
  def t(self):
    """
    Remaining second for countdown mode; or elapsed second for stopwatch mode
    """
    return self._t

  @t.setter
  def t(self, t):

    if self.mode == 'countdown' and t < 0:
      t = 0
    self._t = t

  def start_pause(self):

    if self.started:
      if self.mode == 'countdown':
        self.t -= time.time() - self._started
      else:
        self.t = time.time() - self._started
      self.started = False
      self._emit('paused')
    else:
      if self.mode == 'countdown':
        self._started = time.time()
      else:
        self._started = time.time() - self.t
      self.started = True
      self._emit('started')

  def update(self):

    if self.started:
      self.elapsed = elapsed = time.time() - self._started
      if self.mode == 'stopwatch':
        self.set_text(self.format_text(elapsed))
        return True
      # mode == 'countdown'
      remaining = self.t - elapsed
      if self.no_ds:
        remaining = math.ceil(remaining)
      if remaining <= 0:
        self.started = False
        remaining = 0
      self.set_text(self.format_text(remaining))
      if remaining:
        return True
      else:
        self._emit('ended')

  def set_text(self, text):

    super(TimerWidget, self).set_text(text)
    if not self.no_title:
      print('\33]2;%s\007' % text, end='')

  def keypress(self, size, key):

    if key in ('z', 'a'):
      self.t += 3600 if key == 'a' else -3600
    elif key in ('x', 's'):
      self.t += 60 if key == 's' else -60
    elif key in ('c', 'd'):
      self.t += 1 if key == 'd' else -1
    elif key == ' ':
      self.start_pause()
    else:
      return key
    if not self.started:
      self.set_text(self.format_text(self.t))

  def mouse_event(self, size, event, button, col, row, focus):

    p = self.pack()
    d = col < p[0] and row < p[1]
    if event == 'mouse press' and button == 1 and d:
      self.start_pause()
    else:
      return False


def unhandled_input(key):

  if key in ('q', 'Q'):
    raise UserInterrupt


def update_timer(loop, timer):

  if not timer.started:
    return
  if timer.update():
    loop.set_alarm_in(0.1, update_timer, timer)


def start_update_timer(timer, loop):

  update_timer(loop, timer)


def end_timer(timer, loop):

  raise urwid.ExitMainLoop


TU_SEC = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
RE_TIME_COMP = re.compile('(\d+)([%s])' % ''.join(TU_SEC.keys()), re.I)
RE_TIME_TAG = re.compile('^(?:(?:(\d+):)?(\d+):)?(\d+)$')


def human_time(value):
  '''Convert human readable time to seconds'''

  m = RE_TIME_TAG.match(value)
  if m:
    matches = zip(m.groups(), ('h', 'm', 's'))
    return sum(int(n or 0) * TU_SEC[unit] for n, unit in matches)

  matches = RE_TIME_COMP.findall(value)
  if matches:
    return sum(int(n) * TU_SEC[unit] for n, unit in matches)

  raise argparse.ArgumentTypeError('%r is not a human readable time' % value)


def main():

  parser = argparse.ArgumentParser(description=__description__,
                                   version=__version__)
  parser.add_argument('-d', '--date',
                      help=('calculate countdown from the date, for example, '
                            '"5:35 PM", "noon", or "tomorrow 1 am". '
                            'This option ignores TIME, '
                            'requires parsedatetime package'))
  parser.add_argument('-S', '--stopwatch', action='store_true',
                      help='stopwatch mode')
  parser.add_argument('-s', '--start', action='store_true',
                      help='start timer when %(prog)s starts')
  parser.add_argument('-D', '--no-ds', action='store_true',
                      help='do not show deciseconds (tenth of second)')
  parser.add_argument('-T', '--no-title', action='store_true',
                      help='do not set window title')
  parser.add_argument('-w', '--write-elapsed', metavar='FILE',
                      type=argparse.FileType('w'),
                      help='write final elapsed time to FILE')
  parser.add_argument('time', nargs='*', default=[300], metavar='TIME',
                      type=human_time,
                      help=('countdown started at, '
                            'do not work with stopwatch, '
                            'supported suffixes: d, h, m, s. (default: 5m)'))
  args = parser.parse_args()

  if args.date:
    import datetime as dt
    import parsedatetime.parsedatetime as pdt
    cal = pdt.Calendar()
    sourcetime = cal.parse(args.date)[0]
    d = dt.datetime.fromtimestamp(time.mktime(sourcetime))
    delta = d - dt.datetime.now()
    total_time = int(delta.total_seconds())
    if total_time < 0:
      print('Parsed date of "%s" is %s' % (args.date, d), file=sys.stderr)
      print('Which is in the past and invalid.', file=sys.stderr)
      sys.exit(1)
  else:
    total_time = sum(args.time)

  mode = 'countdown'
  if args.stopwatch:
    mode = 'stopwatch'
    total_time = 0

  timer = TimerWidget(mode=mode, t=total_time, no_ds=args.no_ds,
                      no_title=args.no_title)
  timer_pad = urwid.Padding(timer, align='center', width='clip')
  timer_fill = urwid.Filler(timer_pad)

  loop = urwid.MainLoop(timer_fill, unhandled_input=unhandled_input)
  urwid.connect_signal(timer, 'started', start_update_timer, loop)
  urwid.connect_signal(timer, 'ended', end_timer, loop)
  if args.start:
    timer.start_pause()

  ret = 0
  try:
    loop.run()
  except KeyboardInterrupt:
    ret = 130
  except UserInterrupt:
    if mode == 'countdown':
      ret = 129

  if args.write_elapsed:
    print(timer.elapsed, file=args.write_elapsed)

  return ret

if __name__ == '__main__':
  sys.exit(main())
