import string
import sys
import Tkinter
import Pmw

def _changeNumber(text, factor, increment, min, max):
  value = string.atoi(text)

  if factor > 0:
    if max != '' and value >= max:
      raise ValueError
    value = (value / increment) * increment + increment
  else:
    if min != '' and value <= min:
      raise ValueError
    value = ((value - 1) / increment) * increment

  if min != '' and value < min:
    value = min
  if max != '' and value > max:
    value = max

  return str(value)

def _changeReal(text, factor, increment, min, max):
  value = string.atof(text)

  if factor > 0:
    if max != '' and value >= max:
      raise ValueError
  else:
    if min != '' and value <= min:
      raise ValueError

  # Compare reals using str() to avoid problems caused by binary
  # numbers being only approximations to decimal numbers.
  div = int(value / increment)
  text = str(value)
  if text == str(div * increment) or text == str((div + 1) * increment):
    value = value + factor * increment
  elif factor > 0:
    value = (div + 1) * increment
  else:
    value = div * increment

  if min != '' and value < min:
    value = min
  if max != '' and value > max:
    value = max

  return str(value)

def _changeDate_formatted(format, value, factor, increment, min, max, yyyy):
  jdn = datestringtojdn(value, format) + factor * increment
  if min != '':
    min = datestringtojdn(min, format)
    if jdn < min:
      jdn = min
  if max != '':
    max = datestringtojdn(max, format)
    if jdn > max:
      jdn = max

  y, m, d = jdntoymd(jdn)
  result = ''
  for index in range(3):
    if index > 0:
      result = result + '/'
    f = format[index]
    if f == 'y':
      if yyyy:
        result = result + '%02d' % y
      else:
        result = result + '%02d' % (y % 100)
    elif f == 'm':
      result = result + '%02d' % m
    elif f == 'd':
      result = result + '%02d' % d

  return result

def _changeDate_dmy(value, factor, increment, min, max):
  return _changeDate_formatted('dmy', value, factor, increment, min, max, 0)

def _changeDate_mdy(value, factor, increment, min, max):
  return _changeDate_formatted('mdy', value, factor, increment, min, max, 0)

def _changeDate_ymd(value, factor, increment, min, max):
  return _changeDate_formatted('ymd', value, factor, increment, min, max, 0)

def _changeDate_dmy4(value, factor, increment, min, max):
  return _changeDate_formatted('dmy', value, factor, increment, min, max, 1)

def _changeDate_mdy4(value, factor, increment, min, max):
  return _changeDate_formatted('mdy', value, factor, increment, min, max, 1)

def _changeDate_y4md(value, factor, increment, min, max):
  return _changeDate_formatted('ymd', value, factor, increment, min, max, 1)

def _changeTime24(value, factor, increment, min, max):
    return _changeTimeN(value, factor, increment, min, max, 1)

_SECSPERDAY = 24 * 60 * 60
def _changeTimeN(value, factor, increment, min, max, time24 = 0):
  unixTime = timestringtoseconds(value)
  if factor > 0:
    chunks = unixTime / increment + 1
  else:
    chunks = (unixTime - 1) / increment
  unixTime = chunks * increment
  if time24:
      while unixTime < 0:
	  unixTime = unixTime + _SECSPERDAY
      while unixTime >= _SECSPERDAY:
	  unixTime = unixTime - _SECSPERDAY
  if min != '':
    min = timestringtoseconds(min)
    if unixTime < min:
      unixTime = min
  if max != '':
    max = timestringtoseconds(max)
    if unixTime > max:
      unixTime = max
  if unixTime < 0:
    unixTime = -unixTime
    sign = '-'
  else:
    sign = ''
  secs = unixTime % 60
  unixTime = unixTime / 60
  mins = unixTime % 60
  hours = unixTime / 60
  return '%s%02d:%02d:%02d' % (sign, hours, mins, secs)

def timestringtoseconds(text):
  inputList = string.split(text, ':')
  if len(inputList) != 3:
    raise TypeError, 'invalid value: ' + text

  if inputList[0][0] == '-':
    inputList[0] = inputList[0][1:]
    sign = -1
  else:
    sign = 1
  hour = string.atoi(inputList[0])
  minute = string.atoi(inputList[1])
  second = string.atoi(inputList[2])
  return sign * (hour * 60 * 60 + minute * 60 + second)

_year_pivot = 50
_century = 2000

def setyearpivot(pivot, century = None):
    global _year_pivot
    _year_pivot = pivot
    if century is not None:
	global _century
	_century = century

def datestringtojdn(text, format):
  inputList = string.split(text, '/')
  if len(inputList) != 3:
    raise TypeError, 'invalid value: ' + text

  if format not in ('dmy', 'mdy', 'ymd'):
    raise TypeError, 'invalid format: ' + format
    
  formatList = list(format)
  day = string.atoi(inputList[formatList.index('d')])
  month = string.atoi(inputList[formatList.index('m')])
  year = string.atoi(inputList[formatList.index('y')])

  if _year_pivot is not None:
    if year >= 0 and year < 100:
      if year <= _year_pivot:
	year = year + _century
      else:
	year = year + _century - 100

  return ymdtojdn(year, month, day)

def _cdiv(a, b):
    # Return a / b as calculated by most C language implementations,
    # assuming both a and b are integers.

    if a * b > 0:
	return a / b
    else:
	return -(abs(a) / abs(b))

def ymdtojdn(y, m, d, julian = -1, papal = 1):

    # set Julian flag if auto set
    if julian < 0:
	if papal:                          # Pope Gregory XIII's decree
	    lastJulianDate = 15821004L     # last day to use Julian calendar
	else:                              # British-American usage
	    lastJulianDate = 17520902L     # last day to use Julian calendar

	julian = ((y * 100L) + m) * 100 + d  <=  lastJulianDate

    if y < 0:
	# Adjust BC year
	y = y + 1

    if julian:
	return 367L * y - _cdiv(7 * (y + 5001L + _cdiv((m - 9), 7)), 4) + \
	    _cdiv(275 * m, 9) + d + 1729777L
    else:
	return (d - 32076L) + \
	    _cdiv(1461L * (y + 4800L + _cdiv((m - 14), 12)), 4) + \
	    _cdiv(367 * (m - 2 - _cdiv((m - 14), 12) * 12), 12) - \
	    _cdiv((3 * _cdiv((y + 4900L + _cdiv((m - 14), 12)), 100)), 4) + \
	    1            # correction by rdg

def jdntoymd(jdn, julian = -1, papal = 1):

    # set Julian flag if auto set
    if julian < 0:
	if papal:                          # Pope Gregory XIII's decree
	    lastJulianJdn = 2299160L       # last jdn to use Julian calendar
	else:                              # British-American usage
	    lastJulianJdn = 2361221L       # last jdn to use Julian calendar

	julian = (jdn <= lastJulianJdn);

    x = jdn + 68569L
    if julian:
	x = x + 38
	daysPer400Years = 146100L
	fudgedDaysPer4000Years = 1461000L + 1
    else:
	daysPer400Years = 146097L
	fudgedDaysPer4000Years = 1460970L + 31

    z = _cdiv(4 * x, daysPer400Years)
    x = x - _cdiv((daysPer400Years * z + 3), 4)
    y = _cdiv(4000 * (x + 1), fudgedDaysPer4000Years)
    x = x - _cdiv(1461 * y, 4) + 31
    m = _cdiv(80 * x, 2447)
    d = x - _cdiv(2447 * m, 80)
    x = _cdiv(m, 11)
    m = m + 2 - 12 * x
    y = 100 * (z - 49) + y + x

    # Convert from longs to integers.
    yy = int(y)
    mm = int(m)
    dd = int(d)

    if yy <= 0:
	# Adjust BC years.
	    yy = yy - 1

    return (yy, mm, dd)

# hexadecimal, alphabetic, alphanumeric not implemented
_counterCommands = {
    'numeric'   : _changeNumber,      # } integer
    'integer'   : _changeNumber,      # } these two use the same function
    'real'      : _changeReal,        # real number
    'timeN'     : _changeTimeN,       # HH:MM:SS
    'time24'    : _changeTime24,      # HH:MM:SS (wraps around at 23:59:59)
    'date_dmy'  : _changeDate_dmy,    # DD/MM/YY
    'date_mdy'  : _changeDate_mdy,    # MM/DD/YY
    'date_ymd'  : _changeDate_ymd,    # YY/MM/DD
    'date_dmy4' : _changeDate_dmy4,   # DD/MM/YYYY
    'date_mdy4' : _changeDate_mdy4,   # MM/DD/YYYY
    'date_y4md' : _changeDate_y4md,   # YYYY/MM/DD
}

class Counter(Pmw.MegaWidget):
    # Up-down counter

    # A Counter is a single-line entry widget with Up and Down arrows
    # which increment and decrement the value in the entry.  By
    # default, may be used for entering dates, times, numbers.  User
    # defined functions may be specified for specialised counting.

    def __init__(self, parent = None, **kw):

	# Define the megawidget options.
	INITOPT = Pmw.INITOPT
	optiondefs = (
	    ('autorepeat',     1,             INITOPT),
	    ('buttonaspect',   1.0,           INITOPT),
	    ('datatype',       'numeric',     self._datatype),
	    ('increment',      1,             None),
	    ('initwait',       300,           INITOPT),
	    ('labelmargin',    0,             INITOPT),
	    ('labelpos',       None,          INITOPT),
	    ('max',            '',            None),
	    ('min',            '',            None),
	    ('orient',         'horizontal',  INITOPT),
	    ('padx',           0,             INITOPT),
	    ('pady',           0,             INITOPT),
	    ('repeatrate',     50,            INITOPT),
	)
	self.defineoptions(kw, optiondefs)

	# Initialise the base class (after defining the options).
	Pmw.MegaWidget.__init__(self, parent)

	# Create the components.
	interior = self.interior()

	# If there is no label, put the arrows and the entry directly
	# into the interior, otherwise create a frame for them.  In
	# either case the border around the arrows and the entry will
	# be raised (but not around the label).
	if self['labelpos'] is None:
	    frame = interior
	else:
	    frame = self.createcomponent('frame',
		    (), None,
		    Tkinter.Frame, (interior,))
	    frame.grid(column=2, row=2, sticky='nsew')
	    interior.grid_columnconfigure(2, weight=1)
	    interior.grid_rowconfigure(2, weight=1)

	frame.configure(relief = 'raised', borderwidth = 1)

	# Create the down arrow.
	self._downArrowBtn = self.createcomponent('downarrow',
		(), 'Arrow',
		Tkinter.Canvas, (frame,),
		width = 16, height = 16, relief = 'raised', borderwidth = 2)

	# Create the entry field.
	self._counterEntry = self.createcomponent('entryfield',
		(('entry', 'entryfield_entry'),), None,
		Pmw.EntryField, (frame,))

	# Create the up arrow.
	self._upArrowBtn = self.createcomponent('uparrow',
		(), 'Arrow',
		Tkinter.Canvas, (frame,),
		width = 16, height = 16, relief = 'raised', borderwidth = 2)

	padx = self['padx']
	pady = self['pady']
	if self['orient'] == 'horizontal':
	    self._downArrowBtn.grid(column = 0, row = 0)
	    self._counterEntry.grid(column = 1, row = 0, sticky = 'news')
	    self._upArrowBtn.grid(column = 2, row = 0)
	    frame.grid_columnconfigure(1, weight = 1)
	    frame.grid_rowconfigure(0, weight = 1)
	    if Tkinter.TkVersion >= 4.2:
		frame.grid_columnconfigure(0, pad = padx)
		frame.grid_columnconfigure(2, pad = padx)
		frame.grid_rowconfigure(0, pad = pady)
	else:
	    self._upArrowBtn.grid(column = 0, row = 0)
	    self._counterEntry.grid(column = 0, row = 1, sticky = 'news')
	    self._downArrowBtn.grid(column = 0, row = 2)
	    frame.grid_columnconfigure(0, weight = 1)
	    frame.grid_rowconfigure(1, weight = 1)
	    if Tkinter.TkVersion >= 4.2:
		frame.grid_rowconfigure(0, pad = pady)
		frame.grid_rowconfigure(2, pad = pady)
		frame.grid_columnconfigure(0, pad = padx)

	self.createlabel(interior)

	self._upArrowBtn.bind('<Configure>', self._drawUpArrow)
	self._upArrowBtn.bind('<1>', self._countUp)
	self._upArrowBtn.bind('<Any-ButtonRelease-1>', self._stopUp)
	self._downArrowBtn.bind('<Configure>', self._drawDownArrow)
	self._downArrowBtn.bind('<1>', self._countDown)
	self._downArrowBtn.bind('<Any-ButtonRelease-1>', self._stopDown)
	self._counterEntry.bind('<Configure>', self._resizeArrow)
	entry = self._counterEntry.component('entry')
	entry.bind('<Down>', lambda event, s = self: s.decrement())
	entry.bind('<Up>', lambda event, s = self: s.increment())

	# Initialise instance variables.
	self._flag = 'stopped'
	self._timerId = None

	# Check keywords and initialise options.
	self.initialiseoptions(Counter)

    def _resizeArrow(self, event):
	for btn in (self._upArrowBtn, self._downArrowBtn):
	    bw = (string.atoi(btn['borderwidth']) + \
		    string.atoi(btn['highlightthickness']))
	    newHeight = self._counterEntry.winfo_reqheight() - 2 * bw
	    newWidth = newHeight * self['buttonaspect']
	    btn.configure(width=newWidth, height=newHeight)
	    self._drawArrow(btn)

    def _drawUpArrow(self, event):
	self._drawArrow(self._upArrowBtn)

    def _drawDownArrow(self, event):
	self._drawArrow(self._downArrowBtn)

    def _drawArrow(self, arrow):
	arrow.delete('arrow')

	fg = self._counterEntry.cget('entry_foreground')

	bw = (string.atoi(arrow['borderwidth']) +
		string.atoi(arrow['highlightthickness'])) / 2
	h = string.atoi(arrow['height']) + 2 * bw
	w =  string.atoi(arrow['width']) + 2 * bw

	if arrow == self._downArrowBtn:
	    if self['orient'] == 'horizontal':
		arrow.create_polygon(
		    0.25 * w + bw, 0.5 * h + bw,
		    0.75 * w + bw, 0.25 * h + bw,
		    0.75 * w + bw, 0.75 * h + bw,
		    fill=fg, tag='arrow')
	    else:
		arrow.create_polygon(
		    0.25 * w + bw, 0.25 * h + bw,
		    0.75 * w + bw, 0.25 * h + bw,
		    0.50 * w + bw, 0.75 * h + bw,
		    fill=fg, tag='arrow')
	else:
	    if self['orient'] == 'horizontal':
		arrow.create_polygon(
		    0.75 * w + bw, 0.5 * h + bw,
		    0.25 * w + bw, 0.25 * h + bw,
		    0.25 * w + bw, 0.75 * h + bw,
		    fill=fg, tag='arrow')
	    else:
		arrow.create_polygon(
		    0.5  * w + bw, 0.25 * h + bw,
		    0.25 * w + bw, 0.75 * h + bw,
		    0.75 * w + bw, 0.75 * h + bw,
		    fill=fg, tag='arrow')

    def _countUp(self, event):
	self._upRelief = self._upArrowBtn.cget('relief')
	self._upArrowBtn.configure(relief='sunken')
	self._count(1, 'start')

    def _countDown(self, event):
	self._downRelief = self._downArrowBtn.cget('relief')
	self._downArrowBtn.configure(relief='sunken')
	self._count(-1, 'start')

    def increment(self):
	self._count(1, 'force')

    def decrement(self):
	self._count(-1, 'force')

    def _datatype(self):
	datatype = self['datatype']
	if _counterCommands.has_key(datatype):
	    self._counterCommand = _counterCommands[datatype]
	elif callable(datatype):
	    self._counterCommand = datatype
	else:
	    validValues = _counterCommands.keys()
	    validValues.sort()
	    raise ValueError, ('bad datatype value "%s":  must be a' +
		    ' function or one of %s') % (datatype, validValues)

    def _count(self, factor, newFlag=None):
	if newFlag != 'force':
	  if newFlag is not None:
	    self._flag = newFlag

	  if self._flag == 'stopped':
	    return

	value = self._counterEntry.get()
	try:
	  value = self._counterCommand(value, factor,
		  self['increment'], self['min'], self['max'])
	except:
	  sys.exc_traceback = None   # Clean up object references
	  if newFlag != 'force':
	    if factor == 1:
	      self._upArrowBtn.configure(relief=self._upRelief)
	    else:
	      self._downArrowBtn.configure(relief=self._downRelief)
	    self._flag = 'stopped'
	  self.bell()
	  return

	# If incrementing produces an invalid value, stop counting.
	if not self._counterEntry.setentry(value):
	  self._flag = 'stopped'
	  return

	if newFlag != 'force':
	  if self['autorepeat']:
	    if self._flag == 'start':
	      delay = self['initwait']
	      self._flag = 'running'
	    else:
	      delay = self['repeatrate']
	    self._timerId = self.after(
		delay, lambda self=self, factor=factor: self._count(factor))

    def _stopUp(self, event):
        if self._timerId is not None:
            self.after_cancel(self._timerId)
	    self._timerId = None
        self._upArrowBtn.configure(relief=self._upRelief)
        self._flag = 'stopped'

    def _stopDown(self, event):
        if self._timerId is not None:
            self.after_cancel(self._timerId)
	    self._timerId = None
        self._downArrowBtn.configure(relief=self._downRelief)
        self._flag = 'stopped'

    def destroy(self):
        if self._timerId is not None:
            self.after_cancel(self._timerId)
	    self._timerId = None
        Pmw.MegaWidget.destroy(self)

Pmw.forwardmethods(Counter, Pmw.EntryField, '_counterEntry')