"""HTML <FORM> tag support (and <INPUT>, etc.).

"""

ATTRIBUTES_AS_KEYWORDS = 1

import string
from Tkinter import *
import urllib
import tktools
import ImageWindow

# ------ Forms

URLENCODED = "application/x-www-form-urlencoded"
FORM_DATA = "multipart/form-data"

def start_form(parser, attrs):
    try:
	action = attrs['action']
    except KeyError:
	action = ''
    try:
	method = attrs['method']
    except KeyError:
	method = ''
    try:
	enctype = attrs['enctype']
    except KeyError:
	enctype = URLENCODED
    try:
	target = attrs['target']
    except KeyError:
	target = ''
    form_bgn(parser, action, method, enctype, target)

def end_form(parser):
    form_end(parser)

def do_input(parser, attrs):
    try:
	type = string.lower(attrs['type'])
	del attrs['type']
    except KeyError:
	type = ''
    handle_input(parser, type, attrs)

def start_select(parser, attrs):
    try:
	name = attrs['name']
    except KeyError:
	name = ''
    try:
	size = string.atoi(attrs['size'])
    except (KeyError, string.atoi_error):
	size = 0
    multiple = attrs.has_key('multiple')
    select_bgn(parser, name, size, multiple)

def end_select(parser):
    select_end(parser)

def do_option(parser, attrs):
    try:
	value = attrs['value']
    except KeyError:
	value = ''
    selected = attrs.has_key('selected')
    handle_option(parser, value, selected)

def start_textarea(parser, attrs):
    try:
	name = attrs['name']
    except KeyError:
	name = ''
    try:
	rows = string.atoi(attrs['rows'])
    except (KeyError, string.atoi_error):
	rows = 0
    try:
	cols = string.atoi(attrs['cols'])
    except (KeyError, string.atoi_error):
	cols = 0
    textarea_bgn(parser, name, rows, cols)

def end_textarea(parser):
    textarea_end(parser)

# --- Hooks for forms

def form_bgn(parser, action, method, enctype, target):
    if not hasattr(parser, 'form_stack'):
	parser.form_stack = []
	parser.forms = []
    fi = FormInfo(parser, action, method, enctype, target)
    parser.form_stack.append(fi)

def form_end(parser):
    fi = get_forminfo(parser)
    if fi:
	del parser.form_stack[-1]
	parser.forms.append(fi)
	if not hasattr(parser.context, 'forms'):
	    parser.context.forms = []
	parser.context.forms.append(fi)
	fi.done()

def handle_input(parser, type, options):
    fi = get_forminfo(parser)
    if fi: fi.do_input(type, options)

def select_bgn(parser, name, size, multiple):
    fi = get_forminfo(parser)
    if fi: fi.start_select(name, size, multiple)

def select_end(parser):
    fi = get_forminfo(parser)
    if fi: fi.end_select()

def handle_option(parser, value, selected):
    fi = get_forminfo(parser)
    if fi: fi.do_option(value, selected)

def textarea_bgn(parser, name, rows, cols):
    fi = get_forminfo(parser)
    if fi: fi.start_textarea(name, rows, cols)

def textarea_end(parser):
    fi = get_forminfo(parser)
    if fi: fi.end_textarea()

# --- Form state tacked on the parser

def get_forminfo(parser):
    if hasattr(parser, 'form_stack'):
	if parser.form_stack:
	    return parser.form_stack[-1]
    return None

class FormInfo:

    def __init__(self, parser, action, method, enctype, target):
	self.parser = parser
	self.action = action or ''
	self.method = method or 'get'
	self.enctype = enctype
	self.target = target
	self.context = parser.context
	self.viewer = self.context.viewer
	self.inputs = []
	self.radios = {}
	self.select = None
	self.textarea = None
	self.parser.implied_end_p()
	self.parser.formatter.end_paragraph(1)
	# gather cached form data if we've been to this page before
	formdata_list = self.context.get_formdata()
	if formdata_list and len(formdata_list) > len(parser.forms):
	    self.formdata = formdata_list[len(parser.forms)]
	else:
	    self.formdata = []

    def __del__(self):
	pass				# XXX

    def get(self):
	state = []
	for i in self.inputs:
	    value = i.getstate()
	    name = hasattr(i, 'name') and i.name or ''
	    class_ = i.__class__
	    state.append((class_, name, value))
	return state

    def done(self):			# Called for </FORM>
	if self.parser:
	    self.parser.formatter.end_paragraph(0)
	self.parser = None
	# only restore the form data if it matches, as best as can be
	# determined, the current layout of the form.  Otherwise, just
	# reset the form.
	reset = 1
	if self.formdata and len(self.formdata) == len(self.inputs):
	    for i in self.inputs:
		class_, name, value = self.formdata[0]
		iclass = i.__class__
		iname = hasattr(i, 'name') and i.name or ''
		del self.formdata[0]
		if class_ == iclass and name == iname:
		    i.set(value)
		else:
		    break
	    else:
		reset = None
	if reset:
	    self.reset_command()

    def do_input(self, type, options):
	type = string.lower(type) or 'text'
	classname = 'Input' + string.upper(type[0]) + type[1:]
	if hasattr(self, classname):
	    klass = getattr(self, classname)
	    instance = klass(self, options)
	else:
	    print "*** Form with <INPUT TYPE=%s> not supported ***" % type

    def submit_command(self):
	enctype = string.lower(self.enctype)
	method = string.lower(self.method)
	if method not in ('get', 'post'):
	    print "*** Form with unknown method:", `method`
	    print "Default to method=GET"
	    method = 'get'
	if method == 'get' and enctype == FORM_DATA:
	    print "*** Form with method=GET, enctype=form-data not supported"
	    print "Default to enctype=urlencoded"
	    enctype = URLENCODED
	if enctype not in (URLENCODED, FORM_DATA):
	    print "*** Form with unknown enctype:", `enctype`
	    print "Default to urlencoded"
	    enctype = URLENCODED
	data = ''
	if enctype == URLENCODED:
	    data = self.make_urlencoded_data()
	elif enctype == FORM_DATA and method == 'post':
	    ctype, data = self.make_form_data()
	if method == 'get':
	    url = self.action + '?' + data
	    self.context.follow(url, target=self.target)
	elif method == 'post':
	    if enctype == FORM_DATA:
		enctype = ctype
	    params = {"Content-type": enctype}
	    if enctype == URLENCODED:
		params["Content-length"] = `len(data)`
	    self.viewer.context.post(self.action, data, params, self.target)

    def make_urlencoded_data(self):
	data = ''
	for i in self.inputs:
	    if not i.name: continue
	    v = i.get()
	    if v:
		### images need to return two different values
		### there doesn't seem to be an easy & elegant way to
		###do this
		if type(v) == type(()):
		    s = '&' + quote(i.name + '.x') + '=' + quote(str(v[0]))
		    data = data + s
		    s = '&' + quote(i.name + '.y') + '=' + quote(str(v[1]))
		    data = data + s
		else:
		    if type(v) != type([]):
			v = [v]
		    for vv in v:
			s = '&' + quote(i.name) + '=' + quote(vv)
			data = data + s
	return data[1:]

    def make_form_data(self):
	import ArrayIO
	import MimeWriter
	fp = ArrayIO.ArrayIO()
	mw = MimeWriter.MimeWriter(fp)
	mw.startmultipartbody("form-data")
	for i in self.inputs:
	    if not i.name: continue
	    v = i.get()
	    if type(v) == type(()):
		# XXX Argh!  Have to do it twice, for each coordinate
		if None in v: continue
		disp = 'form-data; name="%s.x"' % i.name
		sw = mw.nextpart()
		sw.addheader("Content-Disposition", disp)
		body = sw.startbody("text/plain")
		body.write(str(v[0]))
		disp = 'form-data; name="%s.y"' % i.name
		sw = mw.nextpart()
		sw.addheader("Content-Disposition", disp)
		body = sw.startbody("text/plain")
		body.write(str(v[1]))
		continue
	    disp = 'form-data; name="%s"' % i.name
	    data = None
	    if i.__class__.__name__ == 'InputFile':
		try:
		    f = open(v)
		    data = f.read()
		    f.close()
		except IOError, msg:
		    print "IOError:", msg
		else:
		    disp = disp + '; filename="%s"' % v
	    sw = mw.nextpart()
	    sw.addheader("Content-Disposition", disp)
	    if data is not None:
		sw.addheader("Content-Length", str(len(data)))
		body = sw.startbody("text/plain")
		body.write(data)
	    else:
		body = sw.startbody("text/plain")
		body.write(v)
	mw.lastpart()
	fp.seek(0)
	import rfc822
	headers = rfc822.Message(fp)
	ctype = headers['content-type']
	ctype = string.join(string.split(ctype)) # Get rid of newlines
	data = fp.read()
	return ctype, data

    def reset_command(self):
	for i in self.inputs:
	    i.reset()

    def start_select(self, name, size, multiple):
	self.select = Select(self, name, size, multiple)

    def end_select(self):
	if self.select:
	    self.select.done()
	    self.select = None

    def do_option(self, value, selected):
	if self.select:
	    self.select.do_option(value, selected)

    def start_textarea(self, name, rows, cols):
	self.textarea = Textarea(self, name, rows, cols)

    def end_textarea(self):
	if self.textarea:
	    self.textarea.done()
	    self.textarea = None

    # The following classes are nested so we can use getattr(self, 'Input...')

    class Input:

	name = ''
	value = ''

	def __init__(self, fi, options):
	    self.fi = fi
	    self.viewer = fi.viewer
	    self.options = options
	    self.getopt('name')
	    self.getopt('value')
	    self.getoptions()
	    self.w = None
	    self.setup()
	    self.reset()
	    self.fi.inputs.append(self)
	    if self.w:
		self.fi.parser.add_subwindow(self.w)

	def getoptions(self):
	    pass

	def setup(self):
	    pass

	def reset(self):
	    pass

	def get(self):
	    return None

	def set(self, value):
	    pass

	def getopt(self, key):
	    if self.options.has_key(key):
		setattr(self, key, self.options[key])

	def getflagopt(self, key):
	    if self.options.has_key(key):
		setattr(self, key, 1)

	def getstate(self):
	    # Get raw state for form caching -- default same as get()
	    return self.get()

    class InputText(Input):

	size = 0
	maxlength = None
	show = None

	def getoptions(self):
	    self.getopt('size')

	def setup(self):
	    self.w = self.entry = Entry(self.viewer.text)
	    self.setup_entry()

	def setup_entry(self):
	    self.entry.bind('<Return>', self.return_event)
	    if self.size:
		size = self.size
		i = string.find(size, ',')
		if i >= 0: size = size[:i]
		try:
		    width = string.atoi(size)
		except string.atoi_error:
		    pass
		else:
		    self.entry['width'] = width
	    if self.show:
		self.entry['show'] = self.show

	def reset(self):
	    self.entry.delete(0, END)
	    self.entry.insert(0, self.value)

	def get(self):
	    return self.entry.get()

	def set(self, value):
	    text = ''
	    if type(value) == type(''):
		text = value
	    elif type(value) == type([]) and len(value) > 0:
		text = value[0]
		del value[0]
	    self.entry.delete(0, END)
	    self.entry.insert(0, text)

	def return_event(self, event):
	    self.fi.submit_command()

    class InputPassword(InputText):

	show = '*'

    class InputCheckbox(Input):

	checked = 0
	value = 'on'

	def getoptions(self):
	    self.getflagopt('checked')

	def setup(self):
	    self.var = StringVar()
	    self.w = Checkbutton(self.viewer.text, variable=self.var,
				 offvalue='', onvalue=self.value)

	def reset(self):
	    self.var.set(self.checked and self.value or '')

	def set(self, value):
	    self.var.set(value)

	def get(self):
	    return self.var.get()

    class InputRadio(InputCheckbox):

	def setup(self):
	    if not self.fi.radios.has_key(self.name):
		self.fi.radios[self.name] = StringVar()
		self.first = 1
	    else:
		self.first = 0
	    self.var = self.fi.radios[self.name]
	    #
	    # N.B. The HTML 2.0 spec is explicit in its description of
	    # what user agents should do when no radio elements are
	    # CHECKED.  See section 8.1.2.4.  But because Netscape and
	    # Mosaic ignore this, many people consider the spec
	    # broken.  If you want to be pedantic, uncomment these two
	    # lines.  With these lines commented out, Grail uses the
	    # same liberal interpretation that those other browsers
	    # employ.
	    #
	    from __main__ import app
	    strict = app.prefs.GetBoolean('parsing-html', 'strict')
	    if strict and self.first:
		self.var.set(self.value)
	    self.w = Radiobutton(self.viewer.text, variable=self.var,
				 value=self.value)

	def reset(self):
	    from __main__ import app
	    strict = app.prefs.GetBoolean('parsing-html', 'strict')
	    if not strict and self.first:
		self.var.set('')
	    if self.checked:
		self.var.set(self.value)

	def get(self):
	    if self.first:
		return self.var.get()
	    else:
		return None

	def getstate(self):
	    # Get raw state for form caching
	    return self.var.get()

    class InputHidden(Input):

	def get(self):
	    return self.value

    class InputImage(Input):

	value = "Image"
	src = None
	alt = '(image)'
	width = 0
	height = 0
	border = 2
	align = ''
	value = (None,None)

	def setup(self):
	    self.getopt('alt')
	    self.getopt('src')
	    self.getopt('width')
	    self.getopt('height')
	    self.getopt('border')
	    self.w = InputImageWindow(self.viewer, self.src, self.alt,
				      self.align, self.width,
				      self.height, self.border,
				      self.set_value_and_submit)

	def get(self):
	    return self.value

	def set_value_and_submit(self, event):
	    self.value = (event.x, event.y)
	    self.fi.submit_command()

    class InputSubmit(Input):

	value = "Submit"

	def setup(self):
	    self.w = Button(self.viewer.text,
			    text=self.value,
			    command=self.submit_command)
	    self.w.bind("<Enter>", self.enter)
	    self.w.bind("<Leave>", self.leave)

	def get(self):
	    if self.w['state'] == ACTIVE:
		return self.value
	    else:
		return None

	def enter(self, event):
	    message = self.fi.action or "???"
	    if self.fi.target:
		message = message + " in " + self.fi.target
	    self.viewer.enter_message(message)

	def leave(self, event):
	    self.viewer.leave_message()

	def submit_command(self):
	    self.viewer.leave_message()
	    self.fi.submit_command()

    class InputReset(Input):

	value = "Reset"

	def setup(self):
	    self.w = Button(self.viewer.text,
			    text=self.value,
			    command=self.fi.reset_command)

    class InputFile(InputText):

	def setup(self):
	    self.w = Frame(self.viewer.text)
	    self.entry = Entry(self.w)
	    self.entry.pack(side=LEFT)
	    self.setup_entry()
	    self.browse = Button(self.w, text="Browse...",
				 command=self.browse_command)
	    self.browse.pack(side=RIGHT)

	def reset(self):
	    # Ignore the initial value from the HTML form.
	    # It is a security hazard.
	    self.entry.delete(0, END)

	def browse_command(self):
	    import FileDialog
	    fd = FileDialog.LoadFileDialog(self.browse)
	    filename = fd.go(self.entry.get(), key="load")
	    if filename:
		self.set(filename)


class Select:

    def __init__(self, fi, name, size, multiple):
	self.fi = fi
	self.viewer = fi.viewer
	self.parser = fi.parser
	self.name = name
	self.size = size
	self.multiple = multiple
	self.option = None
	self.options = []
	self.parser.save_bgn()

    def done(self):
	self.end_option()
	if not len(self.options):
	    self.w = None
	    return
	any = 0
	for v, s, t in self.options:
	    if s: any = 1
	if not any:
	    v, s, t = self.options[0]
	    self.options[0] = v, 1, t
	size = self.size
	if size <= 0:
	    if self.multiple: size = 4
	    else: size = 1
	size = min(len(self.options), size)
	if size == 1 and not self.multiple:
	    self.make_menu()
	else:
	    self.make_list(size)

    def make_menu(self):
	self.v = StringVar()
	self.v.set(self.name)
	values = tuple(map(lambda (v,s,t): t, self.options))
	self.w = apply(OptionMenu,
		       (self.viewer.text, self.v) + values)
	self.reset_menu()
	self.fi.inputs.append(self)
	self.parser.add_subwindow(self.w)

    def make_list(self, size):
	self.v = None
	needvbar = len(self.options) > size
	self.w, self.frame = tktools.make_list_box(self.viewer.text,
						   height=size,
						   vbar=needvbar, pack=0)
	self.w['exportselection'] = 0
	if self.multiple:
	    self.w['selectmode'] = 'extended'
	for v, s, t in self.options:
	    self.w.insert(END, t)
	self.reset_list()
	self.fi.inputs.append(self)
	self.parser.add_subwindow(self.frame)

    def reset(self):
	if not self.w: return
	if self.v:
	    self.reset_menu()
	else:
	    self.reset_list()

    def reset_menu(self):
	for v, s, t in self.options:
	    if s:
		self.v.set(t)
		break

    def reset_list(self):
	self.w.select_clear(0, END)
	for i in range(len(self.options)):
	    v, s, t = self.options[i]
	    if s:
		self.w.select_set(i)

    def get(self):
	# debugging
	if not self.w: return None
	if self.v: return self.get_menu()
	else: return self.get_list()

    def getstate(self):
	return self.get()

    def get_menu(self):
	text = self.v.get()
	for v, s, t in self.options:
	    if text == t: return v or t
	return None

    def get_list(self):
	list = []
	for i in range(len(self.options)):
	    v, s, t = self.options[i]
	    if self.w.select_includes(i):
		list.append(v or t)
	return list

    def set(self, value):
	# debugging
	if not self.w: return
	if self.v: self.set_menu(value)
	else: self.set_list(value)

    def set_menu(self, value):
	for v, s, t in self.options:
	    if value == (v or t):
		self.v.set(t)
		break

    def set_list(self, value):
	self.w.select_clear(0, END)
	for i in range(len(self.options)):
	    v, s, t = self.options[i]
	    if (v or t) in value:
		self.w.select_set(i)

    def do_option(self, value, selected):
	self.end_option()
	self.parser.save_bgn()
	self.option = (value, selected)

    def end_option(self):
	data = string.strip(self.parser.save_end())
	if self.option:
	    value, selected = self.option
	    self.option = None
	    self.options.append((value, selected, data))


class Textarea:

    def __init__(self, fi, name, rows, cols):
	self.fi = fi
	self.parser = fi.parser
	self.viewer = fi.viewer
	self.name = name
	self.rows = rows
	self.cols = cols
	self.parser.push_nofill()
	self.parser.save_bgn()

    def done(self):
	data = self.parser.save_end()
	self.parser.pop_nofill()
	if data[:1] == '\n': data = data[1:]
	if data[-1:] == '\n': data = data[:-1]
	self.w, self.frame = tktools.make_text_box(self.viewer.text,
						   width=self.cols,
						   height=self.rows,
						   hbar=1, vbar=1, pack=0)
	self.w['wrap'] = NONE
	self.data = data
	self.reset()
	self.fi.inputs.append(self)
	self.parser.add_subwindow(self.frame)

    def reset(self):
	self.w.delete("1.0", END)
	self.w.insert(END, self.data)

    def get(self):
	return self.w.get("1.0", END)

    def getstate(self):
	return self.get()

    def set(self, value):
	# TBD: Tk text widget `feature' can cause an extra newline to
	# be inserted each time the text is set.
	if value[-1] == '\n': value = value[:-1]
	self.w.delete("1.0", END)
	self.w.insert(END, value)

def quote(s):
    w = string.splitfields(s, ' ')
    w = map(urllib.quote, w)
    return string.joinfields(w, '+')


class InputImageWindow(Frame):
    """A simple image window that never is an imagemap.

    This is mostly a stripped-down version of ImageWindow used for the
    <IMG> tag. I'm assuming that class can't be gotten at here. Am I
    right? 

    The last argument is a function to bind ButtonRelease-{1,3} to.
    """

    def __init__(self, viewer,
		 src, alt, align, width, height, borderwidth, bind_func):
	self.viewer = viewer
	self.context = self.viewer.context
	self.src, self.alt, self.align = src, alt, align
	bg = viewer.text['background']
	Frame.__init__(self, viewer.text, borderwidth=borderwidth,
		       background=bg)
	self.label = Label(self, text=self.alt, background=bg)
	self.label.pack(fill=BOTH, expand=1)
##	self.pack()
	self.image_loaded = 0
	if width > 0 and height > 0:
	    self.propagate(0)
	    self.config(width=width + 2*borderwidth,
			height=height + 2*borderwidth)
	self.label.bind('<ButtonRelease-1>', bind_func)
	self.label.bind('<ButtonRelease-3>', bind_func)
	self.image = self.context.get_async_image(self.src)
	if self.image:
	    self.label['image'] = self.image

    # may be able to delete this
    def toggle_loading_image(self, event=None):
	if self.image:
	    if hasattr(self.image, 'get_load_status'):
		status = self.image.get_load_status()
		if status == 'loading':
		    self.image.stop_loading()
		else:
		    self.image.start_loading(reload=1)
	else:
	    print "[no image]"
