"""
gavo.formal custom widgets used by the DC (enumerations, table options,
etc)
"""

#c Copyright 2008-2025, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


from twisted.python import components

from twisted.web.template import tags as T

from zope.interface import implementer

from gavo import base
from gavo import formal
from gavo import rscdef
from gavo import utils
from gavo.formal import iformal
from gavo.formal import types as formaltypes
from gavo.formal import widget
from gavo.formal import widgetFactory #noflake: for customWidget
from gavo.formal.util import render_cssid
from gavo.formal.widget import ( #noflake: exported names
	TextInput, Checkbox, Password, TextArea, ChoiceBase, SelectChoice,
	SelectOtherChoice, RadioChoice, CheckboxMultiChoice, FileUpload,
	Hidden)


@implementer(iformal.IWidget)
class DBOptions(object):
	"""A widget that offers limit and sort options for db based cores.

	This is for use in a formal form and goes together with the FormalDict
	type below.
	"""

	sortWidget = None
	limitWidget = None

	def __init__(self, typeOb, service, queryMeta):
		self.service = service
		self.typeOb = typeOb
		if getattr(self.service.core, "sortKey", None) is None:
			self.sortWidget = self._makeSortWidget(service, queryMeta)
			self.directionWidget = self._makeDirectionWidget(service, queryMeta)
		if getattr(self.service.core, "limit", None) is None:
			self.limitWidget = self._makeLimitWidget(service)
		
	def _makeSortWidget(self, service, queryMeta):
		fields = [f for f in self.service.getCurOutputFields(queryMeta,
			raiseOnUnknown=False)]
		if not fields:
			return None
		defaultKey = service.core.getProperty("defaultSortKey", None)
		if defaultKey:
			return SelectChoice(formaltypes.String(),
				options=[(field.name, field.getLabel())
					for field in fields if field.name!=defaultKey],
				noneOption=(defaultKey, defaultKey))
		else:
			return SelectChoice(formaltypes.String(),
				options=[(field.name, field.getLabel()) for field in fields])

	def _makeDirectionWidget(self, service, queryMeta):
		return SelectChoice(formaltypes.String(),
			options=[("DESC", "DESC")],
			noneOption=("ASC", "ASC"))

	def _makeLimitWidget(self, service):
		keys = [(i, str(i)) for i in [1000, 5000, 10000, 100000, 250000]]
		return SelectChoice(formaltypes.Integer(), options=keys,
			noneOption=(100, "100"))

	def render(self, request, key, args, errors):
		# The whole plan sucks -- these should have been two separate widgets
		children = []
		if '_DBOPTIONS' in args:
			# we're working from pre-parsed (nevow formal) arguments
			v = [[args["_DBOPTIONS"]["order"]] or "",
				[args["_DBOPTIONS"]["limit"] or "100"],
				[args["_DBOPTIONS"]["direction"] or "ASC"]]
		else:
			# args come raw from t.w.Request
			v = [args.get(b"_DBOPTIONS_ORDER", [b'']),
				args.get(b"MAXREC", [b"100"]),
				args.get(b"_DBOPTIONS_DIR", b"ASC")]

		if errors:
			# emulate t.w.Request no matter what
			v = [args.get(b"_DBOPTIONS_ORDER", [b'']),
				args.get(b"MAXREC", [b"100"]),
				args.get(b"_DBOPTIONS_DIR", b"ASC")]

		else:
			args = {"_DBOPTIONS_ORDER": utils.debytify(v[0][0]),
				"MAXREC": int(v[1][0]), "_DBOPTIONS_DIR": v[2][0]}

		if self.sortWidget:
			children.extend(["Sort by ",
				self.sortWidget.render(request, "_DBOPTIONS_ORDER", args, errors),
				" "])
			children.extend([" ", self.directionWidget.render(request,
				"_DBOPTIONS_DIR", args, errors)])

		if self.limitWidget:
			children.extend([T.br, "Limit to ",
				self.limitWidget.render(request, "MAXREC", args, errors),
				" items."])
		return T.span(id=render_cssid(key))[children]

	# XXX TODO: make this immutable.
	renderImmutable = render

	def processInput(self, request, key, args, default=''):
		order, limit, direction = None, None, "ASC"
		if self.sortWidget:
			order = self.sortWidget.processInput(request, "_DBOPTIONS_ORDER", args)
		if self.directionWidget:
			direction = self.directionWidget.processInput(
				request, "_DBOPTIONS_DIR", args)
		if self.limitWidget:
			limit = self.limitWidget.processInput(request, "MAXREC", args)
		return {
			"order": order,
			"limit": limit,
			"direction": direction,
		}


class FormalDict(formaltypes.Type):
	"""is a formal type for dictionaries.
	"""
	pass


# I might want to specialise PairOf to have just two items; let's
# see if that's necessary, though.
PairOf = formaltypes.Sequence


class SimpleSelectChoice(SelectChoice):
	def __init__(self, original, options, noneLabel=None):
		if noneLabel is None:
			noneOption = None
		else:
			noneOption = (noneLabel, noneLabel)
		super(SimpleSelectChoice, self).__init__(original,
			[(o,o) for o in options], noneOption)


# MultiSelectChoice is like formal's choice except you can specify a size.

class MultiSelectChoice(SelectChoice):
	size = 3
	def __init__(self, original,  size=None, **kwargs):
		if size is not None:
			self.size=size
		SelectChoice.__init__(self, original, **kwargs)

	def _renderTag(self, request, key, value, converter, disabled):
		if not isinstance(value, (list, tuple)):
			value = [value]

		tag = T.select(name=key, id=render_cssid(key))

		if self.noneOption is not None:
			noneVal = iformal.IKey(self.noneOption).key()
			option = T.option(value=str(noneVal))[
				iformal.ILabel(self.noneOption).label()]
			if value is None or value==noneVal:
				option = option(selected='selected')
			tag[option]

		for item in self.options or []:
			optValue = iformal.IKey(item).key()
			optLabel = iformal.ILabel(item).label()
			optValue = converter.fromType(optValue)
			option = T.option(value=str(optValue))[
				str(optLabel)]
			if optValue in value:
				option = option(selected='selected')
			tag[option]

		if disabled:
			tag(class_='disabled', disabled='disabled')
		return T.span(style="white-space:nowrap")[
			tag(size=str(self.size), multiple="multiple"),
			" ",
			T.span(class_="fieldlegend")[
				"No selection matches all, multiple values legal."]]

	def render(self, request, key, args, errors):
		converter = iformal.IStringConvertible(self.original)
		if errors:
			value = args.get(key, [])
		else:
			value = [converter.fromType(v) for v in args.get(key, [])]
		return self._renderTag(request, key, value, converter, False)

	def processInput(self, request, key, args, default=''):
		values = args.get(key, default.split())
		rv = []
		for value in values:
			value = iformal.IStringConvertible(self.original).toType(value)
			if self.noneOption is not None and value==iformal.IKey(
					self.noneOption).key():
				# NoneOption means "any" here, don't generate a condition
				return None
			rv.append(self.original.validate(value))
		return rv


def _getDisplayOptions(ik):
	"""helps EnumeratedWidget figure out the None option and the options
	for selection.
	"""
	noneOption = None
	options = []
	default = ik.values.default
	if ik.value:
		default = ik.value

	if default is not None:
		if ik.required:
			# default given and field required:  There's no noneOption but a
			# selected default (this shouldn't happen when values.default is gone)
			options = ik.values.options
		else:
			# default given and becomes the noneOption
			for o in ik.values.options:
				if o.content_==ik.values.default:
					noneOption = o
				else:
					options.append(o)
	else:  # no default given, make up ANY option as noneOption unless
	       # ik is required.
		options.extend(ik.values.options)
		noneOption = None
		if not ik.required and not ik.values.multiOk or ik.multiplicity=="multiple":
			noneOption = base.makeStruct(rscdef.Option, title="ANY",
				content_="__DaCHS__ANY__")
	return noneOption, options


def EnumeratedWidget(ik):
	"""a widget factory for input keys over enumerated columns.

	This probably contains a bit too much magic, but let's see.  The current
	rules are:

	If values.multiOk is true, render a MultiSelectChoice, else
	render a SelectChoice or a RadioChoice depending on how many
	items there are.
	
	If ik is not required, add an ANY key evaluating to None.  For
	MultiSelectChoices we don't need this since for them, you can
	simply leave it all unselected.

	If there is a default, it becomes the NoneOption.
	"""
	if not ik.isEnumerated():
		raise base.StructureError("%s is not enumerated"%ik.name)
	noneOption, options = _getDisplayOptions(ik)
	moreArgs = {"noneOption": noneOption}
	if ik.values.multiOk or ik.multiplicity=="multiple":
		if ik.showItems==-1 or len(options)<4:
			baseWidget = CheckboxMultiChoice
			del moreArgs["noneOption"]
		else:
			baseWidget = MultiSelectChoice
			moreArgs["size"] = ik.showItems
			moreArgs["noneOption"] = None
	else:
		if len(options)<4:
			baseWidget = RadioChoice
		else:
			baseWidget = SelectChoice
	
	res = formal.widgetFactory(baseWidget, options=options,
		**moreArgs)
	return res


class StringFieldWithBlurb(widget.TextInput):
	"""is a text input widget with additional material at the side.
	"""
	additionalMaterial = ""

	def __init__(self, *args, **kwargs):
		am = kwargs.pop("additionalMaterial", None)
		widget.TextInput.__init__(self, *args, **kwargs)
		if am is not None:
			self.additionalMaterial = am

	def _renderTag(self, request, key, value, readonly):
		plainTag = widget.TextInput._renderTag(self, request, key, value,
			readonly)
		return T.span(style="white-space:nowrap")[
			plainTag,
			" ",
			T.span(class_="fieldlegend")[self.additionalMaterial]]


class NumericExpressionField(StringFieldWithBlurb):
	additionalMaterial = T.a(href=base.makeSitePath(
			"/static/help_vizier.shtml#floats"))[
		"[?num. expr.]"]


class DateExpressionField(StringFieldWithBlurb):
	additionalMaterial = T.a(href=base.makeSitePath(
			"/static/help_vizier.shtml#dates"))[
		"[?date expr.]"]


class StringExpressionField(StringFieldWithBlurb):
	additionalMaterial = T.a(href=base.makeSitePath(
			"/static/help_vizier.shtml#string"))[
		"[?char expr.]"]


class ScalingTextArea(widget.TextArea):
	"""is a text area that scales with the width of the window.
	"""
	def _renderTag(self, request, key, value, readonly):
		tag=T.textarea(name=key, id=render_cssid(key), rows=str(self.rows),
			style="width:100% !important")[str(value or '')]
		if readonly:
			tag(class_='readonly', readonly='readonly')
		return tag


class Interval(object):
	"""A widget to enter an interval (lower/upper) pair of something.

	As usual with formal widgets, this is constructed with the type, which
	must be PairOf here; we're taking the widget we're supposed to pair from
	it.
	"""
	def __init__(self, original):
		self.original = original
		self.inputType = self.original.type

	def _renderTag(self, request, key, values, readonly):
		tags = []
		for index, seqid in enumerate(["lower", "upper"]):
			tag = T.input(type="text", name=key+seqid,
				id=render_cssid(key+seqid), value=values[index])
			if readonly:
				tag(class_='readonly', readonly='readonly')
# TODO: make a placeholder in the interval
#			if self.placeholder is not None:
#				tag(placeholder=self.placeholder)
			tags.append(tag)

		tags[1:1] = " \u2013 "

		return T.div(class_="form-interval")[tags]

	def render(self, request, key, args, errors):
		if errors:
			values = [args.get(key+"lower", [''])[0],
				args.get(key+"upper", [''])[0]]
		else:
			baseType = iformal.IStringConvertible(self.original)
			values = [baseType.fromType(args.get(key+"lower")),
				baseType.fromType(args.get(key+"upper"))]

		return self._renderTag(request, key, values, False)

	def renderImmutable(self, request, key, args, errors):
		baseType = iformal.IStringConvertible(self.original)
		values = [baseType.fromType(args.get(key+"lower")),
			baseType.fromType(args.get(key+"upper"))]
		return self._renderTag(request, key, values, True)

	def processInput(self, request, key, args, default=["", ""]):
		charset = "utf-8"
		values = [
			args.get(key+"lower", [default[0]])[0].decode(charset),
			args.get(key+"upper", [default[0]])[0].decode(charset)]
		baseType = iformal.IStringConvertible(self.original.type())
		values = [baseType.toType(values[0]),
			baseType.toType(values[1])]
		return self.original.validate(values)


def makeWidgetFactory(code):
	return eval(code)


############# formal adapters for DaCHS objects

# column options
from gavo.rscdef import column

@implementer(iformal.ILabel, iformal.IKey)
class ToFormalAdapter(object):

	def __init__(self, original):
		self.original = original

	def label(self):
		return str(self.original.title)

	def key(self):
		return str(self.original.content_)

components.registerAdapter(ToFormalAdapter, column.Option, iformal.ILabel)
components.registerAdapter(ToFormalAdapter, column.Option, iformal.IKey)
