"""HTML 3.0 <TABLE> tag support.

"""
# $Source: /projects/cvsroot/grail/dist/src/html/table.py,v $
__version__ = '$Id: table.py,v 2.45 1996/05/13 12:42:20 fdrake Exp $'


import string
import regex
import grailutil
from Tkinter import *
from formatter import AbstractWriter, AbstractFormatter
from Viewer import Viewer
from types import *

FIXEDLAYOUT = 1
AUTOLAYOUT = 2
OCCUPIED = 101
EMPTY = 102

BadMojoError = 'Bad Mojo!  Infinite loop in cell height calculation.'

CELLGEOM_RE = regex.compile('%sx%s\+%s\+%s' % (('\([-+]?[0-9]+\)',) * 4))



# ----- HTML tag parsing interface

class TableSubParser:
    def __init__(self):
	self._lasttable = None
	self._table_stack = []

    def start_table(self, parser, attrs):
	# this call is necessary because if a <P> tag is open, table
	# rendering gets totally hosed.  this is caused by the parser
	# not knowing about content model.
	parser.implied_end_p()
	parser.formatter.add_line_break()
	parser.formatter.assert_line_data()
	# tosses any dangling text not in a caption or explicit cell
	parser.save_bgn()
	# create the table data structure
	if self._lasttable:
	    self._table_stack.append(self._lasttable)
	self._lasttable = Table(parser.viewer, attrs, self._lasttable)

    def end_table(self, parser):
	ti = self._lasttable 
	if ti:
	    self._finish_cell(parser)
	    ti.finish()
	    # tosses any dangling text not in a caption or explicit cell
	    parser.save_end()
	    parser.formatter.add_line_break()
	    if self._table_stack:
		self._lasttable = self._table_stack[-1]
		del self._table_stack[-1]
	    else:
		self._lasttable = None

    def start_caption(self, parser, attrs):
	ti = self._lasttable 
	if ti:
	    # tosses any dangling text not in a caption or explicit cell
	    parser.save_end()
	    caption = ti.caption = Caption(ti, parser.viewer, attrs)
	    caption.unfreeze()
	    parser.push_formatter(caption.new_formatter())

    def end_caption(self, parser):
	ti = self._lasttable 
	if ti and ti.caption:
	    # tosses any dangling text not in a caption or explicit cell
	    parser.save_bgn()
	    ti.caption.freeze()
	    parser.pop_formatter()
	    ti.caption.finish()

    def do_colgroup(self, parser, attrs):
	ti = self._lasttable 
	if ti:
	    colgroup = Colgroup(attrs)
	    ti.colgroups.append(colgroup)

    def do_col(self, parser, attrs):
	ti = self._lasttable 
	if ti:
	    col = Col(attrs)
	    if ti.colgroups:
		last_colgroup = ti.colgroups[-1]
		last_colgroup.cols.append(col)
	    else:
		ti.cols.append(col)

    def _do_body(self, parser, attrs):
	ti = self._lasttable
	self._finish_cell(parser)
	body = HeadFootBody(attrs)
	ti.lastbody = body
	return body

    def do_thead(self, parser, attrs):
	ti = self._lasttable 
	if ti: ti.head = self._do_body(parser, attrs)

    def do_tfoot(self, parser, attrs):
	ti = self._lasttable 
	if ti: ti.foot = self._do_body(parser, attrs)

    def do_tbody(self, parser, attrs):
	ti = self._lasttable 
	if ti: ti.tbodies.append(self._do_body(parser, attrs))

    def start_tr(self, parser, attrs):
	self._finish_cell(parser)
	ti = self._lasttable 
	if ti:
	    tr = TR(attrs)
	    if not ti.lastbody:
		# this row goes into an implied tbody
		ti.lastbody = HeadFootBody()
		ti.tbodies.append(ti.lastbody)
	    ti.lastbody.trows.append(tr)
	    ti.lastbody.lastrow = tr

    def end_tr(self, parser):
	ti = self._lasttable
	if ti and ti.lastbody:
	    ti.lastbody.lastrow = None

    def _do_cell(self, parser, attrs, header=None):
	ti = self._lasttable 
	if ti:
	    # finish any previously opened cell
	    self._finish_cell(parser)
	    # create a new object to hold the attributes
	    if not ti.lastbody or not ti.lastbody.lastrow:
		self.start_tr(parser, {})
	    # create a new formatter for the cell, made from a new subviewer
	    if header:
		cell = THCell(ti, parser, attrs)
	    else:
		cell = TDCell(ti, parser, attrs)
	    ti.lastcell = cell
	    cell.unfreeze()
	    parser.push_formatter(cell.new_formatter())
	    # tosses any dangling text not in a caption or explicit cell
	    parser.save_end()
	    #parser.formatter.push_alignment(cell.attribute('align',
	    #					   conv=string.lower))
	    cell.init_style()
	    ti.lastbody.lastrow.cells.append(cell)

    def _finish_cell(self, parser):
	# implicit finish of an open table cell
	ti = self._lasttable
	if ti and ti.lastcell:
	    ti.lastcell.freeze()
	    ti.lastcell.finish()
	    ti.lastcell = None
	    # tosses any dangling text not in a caption or explicit cell
	    parser.save_bgn()
	    parser.pop_formatter()

    def do_th(self, parser, attrs): self._do_cell(parser, attrs, 1)
    def do_td(self, parser, attrs): self._do_cell(parser, attrs)



def conv_stdunits(val):
    """Convert from string representation to Standard Units for Widths.

    pt -- points
    pi -- picas
    in -- inches
    cm -- centimeters
    mm -- millimeters
    em -- em units
    px -- screen pixels (the default)
    %  -- percentage
    *  -- CALS rel. widths

    Units representing, or pre-converted to screen pixels are
    converted to a floating point number.  All other units are
    represented as a tuple (num, repr).

    Note that only screen pixel and percentage units are currently
    handled by the table extension.

    """
    val = string.strip(val)
    if len(val) <= 0:
	return 0
    if val[-1] in ['%', '*']:
	return (grailutil.conv_float(val[:-1]), '%')
    if len(val) <= 1:
	return grailutil.conv_float(val)
    if val[-2:] in ['pt', 'pi', 'in', 'cm', 'mm', 'em']:
	return (grailutil.conv_float(val[:-2]), val[-2:])
    if val[-2:] == 'px':
	val = val[:-2]
    return grailutil.conv_float(val)


class AttrElem:
    """Base attributed table element.

    Common attrs    : id, class, style, lang, dir
    Alignment attrs : align, char, charoff, valign

    """

    def __init__(self, attrs):
	self.attrs = {}
	if type(attrs) == DictType:
	    attrs = attrs.items()
	# if its a dictionary, make a shallow copy, otherwise,
	# dictionary-ize the tuple passed in.
	for k, v in attrs:
	    self.attrs[string.lower(string.strip(k))] = v

    def attribute(self, attr, conv=None, default=None):
	if conv is None:
	    conv = grailutil.conv_integer
	return grailutil.extract_attribute(attr, self.attrs,
					   conv=conv,
					   default=default,
					   delete=None)


def _safe_mojo_height(cell):
    mojocnt = 0
    while mojocnt < 50:
	try:
	    return cell.height()
	except BadMojoError, mojoheight:
## 	    print 'Mojo sez:', mojoheight
	    cell.situate(height=2*mojoheight)
	    mojocnt = mojocnt + 1
    else:
	print 'Not even Mojo knows!  Mojo using:', mojoheight
	return mojoheight



class Container(Canvas):
    def set_table(self, table): self._table = table
    def table_geometry(self):
	"""Return the geometry metrics needed by the table module.

	Return a tuple of the form (MINWIDTH, MAXWIDTH, HEIGHT)
	"""
	return self._table.minwidth(), self._table.maxwidth(), -1


class Table(AttrElem):
    """Top level table information object.

    Attrs: width, cols, border, frame, rules, cellspacing, cellpadding.

    """
    def __init__(self, parentviewer, attrs, parenttable=None):
	AttrElem.__init__(self, attrs)
	self.parentviewer = parentviewer
	self._parenttable = parenttable
	self._cleared = None
	# alignment
	def conv_align(val):
	    return grailutil.conv_enumeration(
		grailutil.conv_normstring(val),
		['left', 'center', 'right'])
	self.Aalign = self.attribute('align', conv=conv_align)
	# this call enforces alignment of the table by inserting a
	# special invisible character right before the embedded window
	# which is the table's container canvas.
	self.parentviewer.prepare_for_insertion(self.Aalign)
	self._mappos = self.parentviewer.text.index('end - 1 c')
	# other attributes
	self.Awidth = self.attribute('width', conv=conv_stdunits)
	self.Acols = self.attribute('cols', conv=grailutil.conv_integer)
	if self.Acols:
## 	    self.layout = FIXEDLAYOUT
	    print 'Fixed layout tables not yet supported!', \
		  '(Using auto-layout)'
	    self.layout = AUTOLAYOUT
	else:
	    self.layout = AUTOLAYOUT
	# grok through the myriad of border/frame combinations.  this
	# is truly grotesque!
	def conv_frame(val):
	    return grailutil.conv_enumeration(
		grailutil.conv_normstring(val),
		['void', 'above', 'below', 'hsides', 'lhs', 'rhs',
		 'vsides', 'box', 'border'])
	Aframe = self.attribute('frame', conv=conv_frame)
	Aborder = self.attribute('border', conv=grailutil.conv_integer)
	if Aborder is None:
	    Aborder = self.attribute('border', conv=grailutil.conv_exists)
	    if Aborder:
		Aborder = 2
	    else:
		Aborder = 0
	# Tk can only handle frames or no frames, it can't do
	# individual or combinations of sides.
	if Aframe is None:
	    if Aborder is None:
		Aframe = 'void'
		borderwidth = 0
		relief = FLAT
	    elif Aborder > 0:
		Aframe = 'border'
		borderwidth = Aborder
		relief = RAISED
	    else:			# Aborder == 0
		Aframe = 'void'
		borderwidth = 0
		relief = FLAT
	elif Aframe == 'void':
	    borderwidth = 0
	    relief = FLAT
	else:
	    if Aborder is None:
		borderwidth = 2
	    else:
		borderwidth = Aborder
	    relief = RAISED
	self.Aframe = Aframe
	self.Aborder = Aborder
	# now do rules attribute
	def conv_rules(val):
	    return grailutil.conv_enumeration(
		grailutil.conv_normstring(val),
		['none', 'groups', 'rows', 'cols', 'all'])
	self.Arules = self.attribute('rules', conv=conv_rules)
	if self.Arules is None:
	    if Aborder == 0:
		self.Arules = 'none'
	    else:
		self.Arules = 'all'
	# cell spacing and padding
	self.Acellspacing = self.attribute('cellspacing',
					   conv=conv_stdunits,
					   default=2)
	self.Acellpadding = self.attribute('cellpadding',
					   conv=conv_stdunits,
					   default=0)
	# geometry
 	self.container = Container(master=parentviewer.text,
				   relief=relief,
				   borderwidth=borderwidth,
				   highlightthickness=0,
				   background=parentviewer.text['background'])
	self.container.set_table(self)

	self.caption = None
	self.cols = []			# multiple COL or COLGROUP
	self.colgroups = []
	self.thead = None
	self.tfoot = None
	self.tbodies = []
	self.lastbody = None
	self.lastcell = None
	self._mapped = None
	# register with the parent viewer
	self.parentviewer.register_reset_interest(self._reset)

    def minwidth(self): return self._minwidth
    def maxwidth(self): return self._maxwidth

    def _map(self):
	if not self._mapped:
	    self.container.pack()
	    pv = self.parentviewer
	    pv.add_subwindow(self.container, index=self._mappos)
	    self._mapped = 1

    def finish(self):
	if self._cleared:
	    return
	if self.layout == AUTOLAYOUT:
	    pv = self.parentviewer
	    self._autolayout_1()
	    self._autolayout_2()
	    self._autolayout_3()
	    if len(pv.context.readers) <= 1:
		# if there are more readers than the one currently
		# loading the page with the table, defer mapping the
		# table
		self._map()
	    pv.context.register_notification(self._notify)
	    self.parentviewer.register_resize_interest(self._resize)
	else:
	    # FIXEDLAYOUT not yet supported
	    pass

    def _autolayout_1(self):
	# internal representation of the table as a sparse array
	self._table = table = {}
	rawtable = {}
	bodies = (self.thead or []) + self.tbodies + (self.tfoot or [])
	bw = self._borderwidth = grailutil.conv_integer(
	    self.container['borderwidth'])

	# pre-populate the table
	for tb in bodies:
	    row = 0
	    for trow in tb.trows:
		col = 0
		for cell in trow.cells:
		    while 1:
			index = (row, col)
			# if the table has an entry for this row and
			# column, then it could only be an OCCUPIED
			# entry.  Keep looking rightward until we find
			# an unoccupied cell.
			if not rawtable.has_key(index):
			    break
			col = col + 1
		    # we've found an unoccupied cell for this one to
		    # reside in.  place it, then occupy any rowspan
		    # and colspan
		    rawtable[index] = cell
		    # the cell could span multiple columns.  TBD:
		    # there must be a better algorithm for this!
		    for cs in range(col+1, col + cell.colspan):
			rawtable[(row, cs)] = OCCUPIED
			for rs in range(row+1, row + cell.rowspan):
			    rawtable[(rs, cs)] = OCCUPIED
		    for rs in range(row+1, row + cell.rowspan):
			rawtable[(rs, col)] = OCCUPIED
			for cs in range(col+1, col + cell.colspan):
			    rawtable[(rs, cs)] = OCCUPIED
		    col = col + 1
		row = row + 1

	# calculate the max number of rows and cols (may not be the
	# pruned number)
	colcount = 0
	rowcount = 0
	for row, col in rawtable.keys():
	    rowcount = max(rowcount, row)
	    colcount = max(colcount, col)
	rowcount = rowcount + 1
	colcount = colcount + 1

	# calculate pruning mask
	rowprune = [0] * rowcount
	colprune = [0] * colcount
	for row in range(rowcount):
	    for col in range(colcount):
		index = (row, col)
		if rawtable.has_key(index) and rawtable[index] <> OCCUPIED:
		    rowprune[row] = 1
		    colprune[col] = 1

	# adjust column and row spans based on pruning
	for row, col in rawtable.keys():
	    index = (row, col)
	    if rawtable[index] == OCCUPIED:
		continue
	    cell = rawtable[index]
	    for prune in rowprune[row:row+cell.rowspan]:
		cell.rowspan = cell.rowspan - 1 + prune
	    for prune in colprune[col:col+cell.colspan]:
		cell.colspan = cell.colspan - 1 + prune

	# prune and fill empty cells
	row = 0
	lastcol = 0
	for rawrow in range(rowcount):
	    rowflag = 0
	    col = 0
	    for rawcol in range(colcount):
		if not rowprune[rawrow] or not colprune[rawcol]:
		    continue
		rowflag = 1
		rawindex = (rawrow, rawcol)
		index = (row, col)
		if not rawtable.has_key(rawindex):
		    table[index] = EMPTY
		else:
		    cell = rawtable[rawindex]
		    if cell == OCCUPIED or not cell.is_empty():
			table[index] = cell
		    else:
			cell.close()
			table[index] = EMPTY
		col = col + 1
		lastcol = max(lastcol, col)
	    if rowflag:
		row = row + 1
	rowcount = row
	colcount = lastcol

	# debugging
## 	print '# of rows=', rowcount, '# of cols=', colcount

## 	print '==========', id(self)
## 	for row in range(rowcount):
## 	    print '[', 
## 	    for col in range(colcount):
## 		element = table[(row, col)]
## 		if element == EMPTY:
## 		    print 'EMPTY', 
## 		elif element == OCCUPIED:
## 		    print 'OCCUPIED',
## 		else:
## 		    print element,
## 	    print ']'
## 	print '==========', id(self)

	# save these for the next phase of autolayout
	self._colcount = colcount
	self._rowcount = rowcount

    def _autolayout_2(self):
	table = self._table
	colcount = self._colcount
	rowcount = self._rowcount
	bw = self._borderwidth

	# calculate column widths
	maxwidths = [0] * colcount
	minwidths = [0] * colcount
	for col in range(colcount):
	    for row in range(rowcount):
		cell = table[(row, col)]
		if cell in [EMPTY, OCCUPIED]:
		    # empty cells don't contribute to the width of the
		    # column and occupied cells have already
		    # contributed to column widths
		    continue
		# cells that span more than one column evenly
		# apportion the min/max widths to each of the
		# consituent columns (this is how Arena does it as per
		# the latest Table HTML spec).
		maxwidth = cell.maxwidth() / cell.colspan
		minwidth = cell.minwidth() / cell.colspan
		for col_i in range(col, col + cell.colspan):
		    maxwidths[col_i] = max(maxwidths[col_i], maxwidth) + bw
		    minwidths[col_i] = max(minwidths[col_i], minwidth) + bw

	# save these for the next phase of autolayout
	self._maxwidths = maxwidths
	self._minwidths = minwidths

    def _autolayout_3(self):
	table = self._table
	colcount = self._colcount
	rowcount = self._rowcount
	bw = self._borderwidth
	maxwidths = self._maxwidths
	minwidths = self._minwidths

	mincanvaswidth = 2 * bw + self.Acellspacing * (colcount + 1)
	maxcanvaswidth = 2 * bw + self.Acellspacing * (colcount + 1)
	for col in range(colcount):
	    mincanvaswidth = mincanvaswidth + minwidths[col]
	    maxcanvaswidth = maxcanvaswidth + maxwidths[col]

	self._minwidth = mincanvaswidth
	self._maxwidth = maxcanvaswidth

	# debugging
## 	print '==========', id(self)
## 	for row in range(rowcount):
## 	    print '[', 
## 	    for col in range(colcount):
## 		element = table[(row, col)]
## 		if element == EMPTY:
## 		    print 'EMPTY', 
## 		elif element == OCCUPIED:
## 		    print 'OCCUPIED',
## 		else:
## 		    print element,
## 	    print ']'
## 	print '==========', id(self)

	availablewidth = self.parentviewer.rule_width()
	if self.Awidth is None:
	    suggestedwidth = availablewidth
	# units in screen pixels
	elif type(self.Awidth) in [IntType, FloatType]:
	    suggestedwidth = self.Awidth
	# other standard units
	elif type(self.Awidth) == TupleType:
	    if self.Awidth[1] == '%':
		suggestedwidth = availablewidth * self.Awidth[0] / 100.0
	    # other standard units are not currently supported
	    else:
		suggestedwidth = veiwerwidth
	else:
	    print 'Tables internal inconsistency.  Awidth=', \
		  self.Awidth, type(self.Awidth)
	    suggestedwidth = availablewidth
	
	# now we need to adjust for the available space (i.e. parent
	# viewer's width).  The Table spec outlines three cases...
	#
	# case 1: the min table width is equal to or wider than the
	# available space.  Assign min widths and let the user scroll
	# horizontally.
	if mincanvaswidth >= suggestedwidth:
	    cellwidths = minwidths
	# case 2: maximum table width fits within the available space.
	# set columns to their maximum width.
	elif maxcanvaswidth < suggestedwidth:
	    cellwidths = maxwidths
	# case 3: maximum width of the table is greater than the
	# available space, but the minimum table width is smaller.
	else:
	    W = suggestedwidth - mincanvaswidth
	    D = maxcanvaswidth - mincanvaswidth
	    adjustedwidths = [0] * colcount
	    for col in range(colcount):
		d = maxwidths[col] - minwidths[col]
		adjustedwidths[col] = minwidths[col] + d * W / D
	    cellwidths = adjustedwidths

	# calculate column heights.  this should be done *after*
	# cellwidth calculations, due to side-effects in the cell
	# algorithms
	cellheights = [0] * rowcount

	for row in range(rowcount):
	    for col in range(colcount):
		cell = table[(row, col)]
		if cell in [EMPTY, OCCUPIED]:
		    continue
		cellwidth = self.Acellspacing * (cell.colspan - 1)
		for w in cellwidths[col:col + cell.colspan]:
		    cellwidth = cellwidth + w
		cell.situate(width=cellwidth)
		cellheight = _safe_mojo_height(cell) / cell.rowspan
		for row_i in range(row, min(rowcount, row + cell.rowspan)):
		    cellheights[row_i] = max(cellheights[row_i], cellheight)

	canvaswidth = self.Acellspacing * (colcount - 1)
	for col in range(colcount):
	    canvaswidth = canvaswidth + cellwidths[col]

	ypos = bw + self.Acellspacing

	# if caption aligns top, then insert it now.  it doesn't need
	# to be moved, just resized
	if self.caption and self.caption.align <> 'bottom':
	    if canvaswidth < 0:
		canvaswidth = self.parentviewer.rule_width()
	    # must widen before calculating height!
	    self.caption.situate(width=canvaswidth)
	    height = self.caption.height()
	    self.caption.situate(x=bw, y=ypos, height=height)
	    ypos = ypos + height + self.Acellspacing

	# now place and size each cell
	for row in range(rowcount):
	    xpos = bw + self.Acellspacing
	    tallest = 0
	    for col in range(colcount):
		cell = table[(row, col)]
		if cell in [EMPTY, OCCUPIED]:
		    xpos = xpos + cellwidths[col] + self.Acellspacing
		    continue
		rowspan = min(rowcount, row + cell.rowspan)
		cellheight = self.Acellspacing * (rowspan - row - 1)
		for h in cellheights[row:min(rowcount, row + cell.rowspan)]:
		    cellheight = cellheight + h
		cell.situate(x=xpos, y=ypos, height=cellheight)
		xpos = xpos + cellwidths[col] + self.Acellspacing
	    ypos = ypos + cellheights[row] + self.Acellspacing

	# if caption aligns bottom, then insert it now.  it needs to
	# be resized and moved to the proper location.
	if self.caption and self.caption.align == 'bottom':
	    if canvaswidth < 0:
		canvaswidth = self.parentviewer.rule_width()
	    # must widen before calculating height!
	    self.caption.situate(width=canvaswidth)
	    height = self.caption.height()
	    self.caption.situate(x=bw, y=ypos, height=height)
	    ypos = ypos + height + self.Acellspacing

	self.container.config(width=canvaswidth + 2 * self.Acellspacing,
			      height=ypos-bw)

    def _reset(self, viewer):
	# called when the viewer is cleared
	self._cleared = 1
##  	print '_reset:', self, viewer, self._cleared
	self.parentviewer.context.unregister_notification(self._notify)
	self.parentviewer.unregister_reset_interest(self._reset)
	self.parentviewer.unregister_resize_interest(self._resize)
	delattr(self.container, '_table')
	# TBD: garbage collect internal structures, but not windows!

    def _resize(self, viewer):
	# called when the outer browser is resized (typically by the user)
## 	print '_resize:', viewer
	self._autolayout_3()

    def _notify(self, context):
	# receives notification when all readers for the shared
	# context have finished.  this typically occurs when there are
	# images inside table cells.  it will also happen for every
	# table cell exactly once, but if there are no embedded
	# images, the actual resize will be inhibited.
	recalc_needed = None
	for row in range(self._rowcount):
	    for col in range(self._colcount):
		cell = self._table[(row, col)]
		if cell in [EMPTY, OCCUPIED]:
		    continue
		status = cell.recalc()
		recalc_needed = recalc_needed or status
	if recalc_needed:
	    self._autolayout_2()
	    self._autolayout_3()
	if not self._mapped:
	    self._map()
	    

class Colgroup(AttrElem):
    """A column group."""
    def __init__(self, attrs):
	AttrElem.__init__(self, attrs)
	self.cols = []

class Col(AttrElem):
    """A column."""

class HeadFootBody(AttrElem):
    """A generic THEAD, TFOOT, or TBODY."""

    def __init__(self, attrs=[]):
	AttrElem.__init__(self, attrs)
	self.trows = []
	self.lastrow = None

class TR(AttrElem):
    """A TR table row element."""

    def __init__(self, attrs):
	AttrElem.__init__(self, attrs)
	self.cells = []


def _get_linecount(tw):
    return string.atoi(string.splitfields(tw.index(END), '.')[0]) - 1

def _get_widths(tw):
    width_max = 0
    border_x = None
    # get maximum width of cell: the longest line with no line wrapping
    tw['wrap'] = NONE
    border_x, y, w, h, b = tw.dlineinfo(1.0)
    linecnt = _get_linecount(tw) + 1
    for lineno in range(1, linecnt):
	index = '%d.0' % lineno
	tw.see(index)
	x, y, w, h, b = tw.dlineinfo(index)
	width_max = max(width_max, w)
    width_max = width_max + (2 * border_x)
    # get minimum width of cell: longest word
    tw['wrap'] = WORD
    contents = tw.get(1.0, END)
    longest_word = reduce(max, map(len, string.split(contents)), 0)
    tw['width'] = longest_word + 1
    width_min = tw.winfo_reqwidth() + (2 * border_x)
    return float(width_min), float(width_max)

def _get_height(tw):
    linecount = _get_linecount(tw)
    tw['height'] = linecount
    tw.update_idletasks()
    tw.see(1.0)
    x, border_y, w, other_h, b = tw.dlineinfo(1.0)
    loopcnt = 0
    while 1:
	tw.see(1.0)
	info = tw.dlineinfo('end - 1 c')
	if info:
	    x, y, w, h, b = info
	    if h >= b:
		break
	# TBD: loopcnt check is probably unnecessary, but I'm not yet
	# convinced this algorithm always works.
	loopcnt = loopcnt + 1
	if loopcnt > 25:
	    raise BadMojoError, tw.winfo_height()
	linecount = linecount + 1
	tw['height'] = linecount
	tw.update_idletasks()
    # TBD: this isn't quite right.  We want to add border_y, but
    # that's not correct for the lower border.  I think we can ask the
    # textwidget for it's internal border space, but we may need to
    # add in relief space too.  Close approximation for now...
    #
    # Add 2 for descenders
    return y+h+border_y + 2



class ContainedText(AttrElem):
    """Base class for a text widget contained as a cell in a canvas.
    Both Captions and Cells are derived from this class.

    """
    def __init__(self, table, parentviewer, attrs):
	AttrElem.__init__(self, attrs)
	self._table = table
	self._container = table.container
	self._viewer = Viewer(master=table.container,
			      context=parentviewer.context,
			      scrolling=0,
			      stylesheet=parentviewer.stylesheet,
			      parent=parentviewer)
	self._viewer.RULE_WIDTH_MAGIC = self._viewer.RULE_WIDTH_MAGIC - 6
	# for callback notification
	self._fw = self._viewer.frame
	self._tw = self._viewer.text
	self._tw.config(highlightthickness=0)
	self._width = 0
	self._embedheight = 0

    def new_formatter(self):
	return AbstractFormatter(self._viewer)

    def freeze(self): self._viewer.freeze()
    def unfreeze(self): self._viewer.unfreeze()
    def close(self): self._viewer.close()

    def maxwidth(self):
	return self._maxwidth		# not useful until after finish()
    def minwidth(self):
	return self._minwidth		# likewise

    def height(self):
	return max(self._embedheight, _get_height(self._tw))

    def recalc(self):
	# recalculate width and height upon notification of completion
	# of all context's readers (usually image readers)
	min_nonaligned = self._minwidth
	maxwidth = self._maxwidth
	embedheight = self._embedheight
	# take into account all embedded windows
	for sub in self._viewer.subwindows:
	    # the standard interface is used if the object has a
	    # table_geometry() method
	    if hasattr(sub, 'table_geometry'):
		submin, submax, height = sub.table_geometry()
		min_nonaligned = max(min_nonaligned, submin)
		maxwidth = max(maxwidth, submax)
		embedheight = max(embedheight, height)
	    else:
		# this is the best we can do
## 		print 'non-conformant embedded window:', sub.__class__
## 		print 'using generic method, which may be incorrect'
		geom = sub.winfo_geometry()
		if CELLGEOM_RE.search(geom) >= 0:
		    [w, h, x, y] = map(grailutil.conv_integer,
				       CELLGEOM_RE.group(1, 2, 3, 4))
		min_nonaligned = max(min_nonaligned, w)	# x+w?
		maxwidth = max(maxwidth, w)             # x+w?
		embedheight = max(embedheight, h)       # y+h?
	self._embedheight = embedheight
	self._minwidth = min_nonaligned
	self._maxwidth = maxwidth
	return len(self._viewer.subwindows)

    def finish(self, padding=0):
	# TBD: if self.layout == AUTOLAYOUT???
	self._x = self._y = 0
	fw = self._fw
	tw = self._tw
	# set the padding before grabbing the width
	tw['padx'] = padding
	# TBD: according to the W3C table spec, minwidth should really
	# be max(min_left + min_right, min_nonaligned).  Also note
	# that minwidth is recalculated by minwidth() call
	self._minwidth, self._maxwidth = _get_widths(self._tw)
	# first approximation of height.  this is the best we can do
	# without forcing an update_idletasks() fireworks display
	tw['height'] = _get_linecount(tw) + 1
	# initially place the cell in the canvas at position (0,0),
	# with the maximum width and closest approximation height.
	# situate() will be called later with the final layout
	# parameters.
	self._tag = self._container.create_window(
	    0, 0,
	    window=fw, anchor=NW,
	    width=self._maxwidth,
	    height=fw['height'])

    def situate(self, x=0, y=0, width=None, height=None):
	# canvas.move() deals in relative positioning, but we want
	# absolute coordinates
	xdelta = x - self._x
	ydelta = y - self._y
	self._x = x
	self._y = y
	self._container.move(self._tag, xdelta, ydelta)
	if width <> None and height <> None:
	    self._container.itemconfigure(self._tag,
					  width=width, height=height)
	elif width <> None:
	    self._container.itemconfigure(self._tag, width=width)
	else:
	    self._container.itemconfigure(self._tag, height=height)


class Caption(ContainedText):
    """A table caption element."""
    def __init__(self, table, parentviewer, attrs):
	ContainedText.__init__(self, table, parentviewer, attrs)
	self._tw.config(relief=FLAT, borderwidth=0)
	def conv_align(val):
	    return grailutil.conv_enumeration(
		grailutil.conv_normstring(val),
		['top', 'bottom', 'left', 'right']) or 'top'
	self.align = self.attribute('align', conv=conv_align)

    def finish(self, padding=0):
	ContainedText.finish(self, padding=0)
	# set the style of the contained text
	self._viewer.text.tag_add('contents', 1.0, END)
	self._viewer.text.tag_config('contents', justify=CENTER)


class Cell(ContainedText):
    """A generic TH or TD table cell element."""

    def __init__(self, table, parser, attrs):
	ContainedText.__init__(self, table, parser.viewer, attrs)
	self._parser = parser
	# relief and borderwidth are defined as table tag attributes
	if table.Arules == 'none':
	    relief = FLAT
	# TBD: rules=rows and rules=cols not yet implemented (is it
	# even possible in Tk?  probably, but could be painful
	else:
	    if table.Aframe == 'void':
		relief = FLAT
	    else:
		relief = SUNKEN
	self._tw.config(relief=relief, borderwidth=1)
	self.layout = table.layout
	# dig out useful attributes
	self.cellpadding = table.attribute('cellpadding', 0)
	self.rowspan = self.attribute('rowspan', default=1)
	self.colspan = self.attribute('colspan', default=1)
	if self.cellpadding < 0:
	    self.cellpadding = 0
	if self.rowspan < 0:
	    self.rowspan = 1
	if self.colspan < 0:
	    self.colspan = 1

    def init_style(self):
	pass

    def __repr__(self):
##	return '<%s>' % id(self) + '"%s"' % self._tw.get(1.0, END)[:-1]
	return '"%s"' % self._tw.get(1.0, END)[:-1]

    def is_empty(self):
	return not self._tw.get(1.0, 'end - 1 c')

    def finish(self, padding=0):
	ContainedText.finish(self, padding=self.cellpadding)


class TDCell(Cell):
    pass

class THCell(Cell):
    def init_style(self):
	# TBD: this should be extracted from stylesheets and/or preferences
	self._parser.get_formatter().push_font((None, None, 1, None))

    def finish(self):
	Cell.finish(self)
	self._tw.tag_add('contents', 1.0, END)
	self._tw.tag_config('contents', justify=CENTER)


if __name__ == '__main__':
    pass
else:
    tparser = TableSubParser()
    for attr in dir(TableSubParser):
	if attr[0] <> '_':
	    exec '%s = tparser.%s' % (attr, attr)
