"""
A renderer for Data to HTML/stan
"""

#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.


import itertools
import os
import re
import urllib.parse

from twisted.web import template
from twisted.web.template import tags as T

from gavo import base
from gavo import formal
from gavo import formats
from gavo import rsc
from gavo import svcs
from gavo import utils
from gavo.base import valuemappers
from gavo.formal import nevowc
from gavo.formats import texttable
from gavo.protocols import products
from gavo.rscdef import rmkfuncs
from gavo.utils import serializers
from gavo.utils import typeconversions
from gavo.web import common


_htmlMFRegistry = texttable.displayMFRegistry.clone()
_registerHTMLMF = _htmlMFRegistry.registerFactory
NT = nevowc.addNevowAttributes


def _barMapperFactory(colDesc):
	if colDesc["displayHint"].get("type")!="bar":
		return
	def coder(val):
		if val:
			return T.hr(style="width: %dpx"%int(val), title="%.2f"%val,
				class_="scoreBar")
		return ""
	return coder
_registerHTMLMF(_barMapperFactory)


def _productMapperFactory(colDesc):
	if colDesc["displayHint"].get("type")!="product":
		return
	if colDesc["displayHint"].get("nopreview"):
		mouseoverHandler = None
	else:
		mouseoverHandler = "insertPreview(this, null)"
	fixedArgs = ""
	defaultAnchor = colDesc.original.getProperty("anchorText", None)
	
	baseURL = base.getCurrentServerURL()

	def coder(val):
		if val:
			anchor = defaultAnchor
			if anchor is None:
				anchor = re.sub(r"\?.*", "",
						os.path.basename(urllib.parse.unquote_plus(str(val)[4:])))
				if not anchor:
					anchor = "File"

			if isinstance(val, str) and utils.looksLikeURLPat.match(val):
				# readily inserted URLs probably can't do previews, and
				# we must not direct them through the product table anyway.
				return T.a(href=val)[anchor]

			else:
				return T.a(
					href=products.makeProductLink(val, useHost=baseURL)+fixedArgs,
					onmouseover=mouseoverHandler,
					class_="productlink")[anchor]
		else:
			return ""
	return coder
_registerHTMLMF(_productMapperFactory)


def _simbadMapperFactory(colDesc):
	"""is a mapper yielding links to simbad.

	To make this work, you need to furnish the OutputField with a
	select="array[alphaFloat, deltaFloat]" or similar.

	You can give a coneMins displayHint to specify the search radius in
	minutes.
	"""
	if colDesc["displayHint"].get("type")!="simbadlink":
		return
	radius = float(colDesc["displayHint"].get("coneMins", "1"))
	def coder(data):
		if data is None:
			return ""

		alpha, delta = data[0], data[1]
		if alpha and delta:
			return T.a(href="http://simbad.u-strasbg.fr/simbad/sim-coo?Coord=%s"
				"&Radius=%f"%(urllib.parse.quote("%.5fd%+.5fd"%(alpha, delta)),
					radius))["[Simbad]"]
	return coder
_registerHTMLMF(_simbadMapperFactory)


def _bibcodeMapperFactory(colDesc):
	if not (colDesc["displayHint"].get("type")=="bibcode"
			or colDesc["ucd"].startswith("meta.bib.bibcode")):
		return
	def coder(data):
		if data:
			for item in data.split(","):
				item = item.strip()
				if utils.couldBeABibcode(item):
					urlBibcode = urllib.parse.quote(item.strip())
					adsLink = f"https://ui.adsabs.harvard.edu/abs/{urlBibcode}/abstract"
					yield T.a(href=adsLink)[item.strip()]
				else:
					yield item
				yield " "
		else:
			yield ""
	return coder
_registerHTMLMF(_bibcodeMapperFactory)


def _keepHTMLMapperFactory(colDesc):
	if colDesc["displayHint"].get("type")!="keephtml":
		return
	def coder(data):
		if data:
			return T.xml(data)
		return ""
	return coder
_registerHTMLMF(_keepHTMLMapperFactory)


def _imageURLMapperFactory(colDesc):
	if colDesc["displayHint"].get("type")!="imageURL":
		return
	width = colDesc["displayHint"].get("width")
	def coder(data):
		if data:
			res = T.img(src=data, alt="Image at %s"%data)
			if width:
				res(width=width)
			return res
		return ""
	return coder
_registerHTMLMF(_imageURLMapperFactory)


def _urlMapperFactory(colDesc):
	if colDesc["displayHint"].get("type")!="url":
		return

	anchorText = colDesc.original.getProperty("anchorText", None)
	if anchorText:
		def makeAnchor(data):
			return anchorText
	else:
		def makeAnchor(data): #noflake: conditional definition
			return urllib.parse.unquote(
				urllib.parse.urlparse(data)[2].split("/")[-1])

	def coder(data):
		if data:
			return T.a(href=data)[makeAnchor(data)]
		return ""
	return coder
_registerHTMLMF(_urlMapperFactory)


def _booleanCheckmarkFactory(colDesc):
	"""inserts mappers for values with displayHint type=checkmark.

	These render a check mark if the value is python-true, else nothing.
	"""
	if colDesc["displayHint"].get("type")!="checkmark":
		return
	def coder(data):
		if data:
			return "\u2713"
		return ""
	return coder
_registerHTMLMF(_booleanCheckmarkFactory)


def _pgSphereMapperFactory(colDesc):
	"""do a reasonable representation of arrays in HTML:
	"""
	if not colDesc["dbtype"] in serializers.GEOMETRY_TYPES:
		return

	def mapper(val):
		if val is None:
			return None
		return T.span(class_="array")["[%s]"%" ".join(
			"%s"%v for v in val.asDALI())]

	colDesc["datatype"], colDesc["arraysize"], colDesc["xtype"
		] = typeconversions.sqltypeToVOTable(colDesc["dbtype"])

	return mapper
_registerHTMLMF(_pgSphereMapperFactory)


#  Insert new, more specific factories here


class HeadCellsMixin(nevowc.CommonRenderers):
	"""A mixin providing renders for table headings.

	The class mixing in must give the SerManager used in a serManager
	attribute.
	"""
	def data_fielddefs(self, request, tag):
		return self.serManager.table.tableDef.columns

	@template.renderer
	def headCell(self, request, tag):
		colDef = tag.slotData

		# work with OutputFields, too
		if hasattr(colDef, "key"):
			colDef = self.serManager.getColumnByName(colDef.key)

		cont = colDef.original.getLabel()
		desc = colDef["description"]
		if not desc:
			desc = cont
		tag = tag(title=desc)[T.xml(cont)]
		if colDef["unit"]:
			tag[T.br, "[%s]"%colDef["unit"]]
		note = colDef["note"]
		if note:
			noteURL = "#note-%s"%note.tag
			tag[T.sup[T.a(href=noteURL)[note.tag]]]
		return tag


class HeadCells(template.Element, HeadCellsMixin):
	def __init__(self, serManager):
		self.serManager = serManager

	loader = nevowc.XMLString("""
		<tr xmlns:n="http://nevow.com/ns/nevow/0.1"
			n:render="sequence" n:data="fielddefs">
			<th n:pattern="item" n:render="headCell" class="thVertical"/>
		</tr>""")


_htmlMetaBuilder = common.HTMLMetaBuilder()


def _compileRenderer(source, queryMeta, rd):
	"""returns a function object from source.

	Source must be the function body of a renderer.  The variable data
	contains the entire row, and the thing must return a string or at
	least stan (it can use T.tag).
	"""
	code = ("def format(data):\n"+
		utils.fixIndentation(source, "  ")+"\n")
	return rmkfuncs.makeProc("format", code, "", None,
		"renderer "+utils.makeEllipsis(code),
		queryMeta=queryMeta, source=source, T=T, rd=rd)


class HTMLDataRenderer(formal.NevowcElement):
	"""A base class for rendering tables and table lines.

	Both HTMLTableFragment (for complete tables) and HTMLKeyValueFragment
	(for single rows) inherit from this.
	"""
	def __init__(self, table, queryMeta):
		self.table, self.queryMeta = table, queryMeta
		super(HTMLDataRenderer, self).__init__()
		self._computeSerializationRules()
		self._makeSerializer()

	def _computeSerializationRules(self):
		"""creates the serialization manager and the formatter sequence.

		These are in the attributes serManager and formatterSeq, respectively.
		formatterSeq consists of triples of (name, formatter, fullRow), where
		fullRow is true if the formatter wants to be passed the full row rather
		than just the column value.
		"""
		self.serManager = valuemappers.SerManager(self.table, withRanges=False,
			mfRegistry=_htmlMFRegistry, acquireSamples=False)
		self.formatterSeq = []
		for index, (desc, field) in enumerate(
				zip(self.serManager, self.table.tableDef)):
			formatter = self.serManager.mappers[index]
			if isinstance(field, svcs.OutputField):
				if field.wantsRow:
					desc["wantsRow"] = True
				if field.formatter:
					formatter = _compileRenderer(
						field.formatter,
						self.queryMeta,
						self.table.tableDef.rd)
			self.formatterSeq.append(
				(desc["name"], formatter, desc.get("wantsRow", False)))

	def _makeSerializer(self):
		"""adds a serialiseRow attribute containing a function that turns
		a table row into a sequence embeddable into stan trees.
		"""
		source = [
			"def serializeRow(row):",
			"  res = []",]
		for index, (name, _, wantsRow) in enumerate(self.formatterSeq):
			if wantsRow:
				source.append("  val = formatters[%d](row)"%index)
			else:
				source.append("  val = formatters[%d](row[%s])"%(index, repr(name)))
			source.append(
				"  res.append('N/A' if val is None else val)")

		source.append("  return res")
		self.serializeRow = utils.compileFunction(
			"\n".join(source), "serializeRow", {
				"formatters": [p[1] for p in self.formatterSeq]})

	@template.renderer
	def footnotes(self, request, tag):
		"""renders the footnotes as a definition list.
		"""
		if self.serManager.notes:
			yield T.hr(class_="footsep")
			yield T.dl(class_="footnotes")[[
				T.xml(note.getContent(targetFormat="html",
					macroPackage=self.serManager.table.tableDef))
				for t, note in sorted(self.serManager.notes.items())]]

	def data_fielddefs(self, request, tag):
		return self.table.tableDef.columns

	@template.renderer
	def meta(self, request, tag):
		metaKey = tag.children[0]
		if self.table.getMeta(metaKey, propagate=False):
			tag.clear()
			_htmlMetaBuilder.clear()
			return tag[self.table.buildRepr(metaKey, _htmlMetaBuilder)]
		else:
			return ""


class HTMLTableFragment(HTMLDataRenderer):
	"""A nevow renderer for result tables.
	"""
	rowsPerDivision = 25

	def __init__(self, table, queryMeta):
		HTMLDataRenderer.__init__(self, table, queryMeta)
		self._computeHeadCellsStan()

	def _computeHeadCellsStan(self):
		rendered = HeadCells(self.serManager)
		# We're caching the computed head cells in hopes that things are
		# a bit faster.
		self.headCellsStan = T.xml(nevowc.flattenSync(rendered))

	@template.renderer
	def headCells(self, request, tag):
		"""returns the header line for this table as an XML string.
		"""
		return tag[self.headCellsStan]

	def _formatRow(self, row, rowAttrs):
		"""returns row HTML-rendered.
		"""
		res = ['<tr%s>'%rowAttrs]
		for val in self.serializeRow(row):
		  if isinstance(val, (str, bytes)):
		    serFct = utils.escapePCDATA
		  else:
		    serFct = nevowc.flattenSyncToString
		  res.append('<td>%s</td>'%serFct(val))
		res.append('</tr>')
		return ''.join(res)

	@template.renderer
	def rowSet(self, request, tag):
		# slow, rather use tableBody
		return tag(render="sequence")[
			NT(T.td, pattern="item")(render="unicode")]

	@template.renderer
	def tableBody(self, request, tag):
		"""returns HTML-rendered table rows in chunks of rowsPerDivision.

		We don't use stan here since we can concat all those tr/td much faster
		ourselves.
		"""
		rowAttrsIterator = itertools.cycle([' class="data"', ' class="data even"'])
		rendered = []
		yield T.xml("<tbody>")
		for row in self.table:
			rendered.append(self._formatRow(row, next(rowAttrsIterator)))
			if len(rendered)>=self.rowsPerDivision:
				yield T.xml("\n".join(rendered))
				yield self.headCellsStan
				rendered = []
		yield T.xml("\n".join(rendered)+"\n</tbody>")

	loader = template.TagLoader(T.div(class_="tablewrap")[
		T.div(render="meta", class_="warning")["_warning"],
		T.table(class_="results") [
				T.thead(render="headCells"),
				T.tbody(render="tableBody")],
			T.transparent(render="footnotes"),
		]
	)


class HTMLKeyValueFragment(HTMLDataRenderer, HeadCellsMixin):
	"""A nevow renderer for single-row result tables.
	"""
	def data_firstrow(self, request, tag):
		"""returns a sequence for (colDef, serialised value) for the first row
		of the result table.
		"""
		return list(zip(self.serManager, self.serializeRow(self.table.rows[0])))

	@template.renderer
	def coldefdesc(self, request, tag):
		"""returns the description of a colDev in a firstrow sequence.
		"""
		return tag[tag.slotData[0].original.description]

	def makeLoader(self):
		return template.TagLoader([
			T.div(render="meta", class_="warning")["_warning"],
				NT(T.table, data="firstrow")(class_="keyvalue", render="sequence")[
					NT(T.transparent, pattern="item")[
						T.tr[
							NT(T.th, data="0")(render="headCell", class_="thHorizontal"),
							NT(T.td, data="1")(class_="data", render="passthrough")
						],
						T.tr(class_="keyvaluedesc")[
							T.td(colspan="2", render="coldefdesc")]
			]],
			T.transparent(render="footnotes"),
			])
	
	loader = property(makeLoader)


def writeDataAsHTML(data, outputFile, acquireSamples=False):
	"""writes data's primary table to outputFile.

	(acquireSamples is actually ignored; it is just present for compatibility
	with the other writers until I rip out the samples stuff altogether).
	"""
	if isinstance(data, rsc.Data):
		data = data.getPrimaryTable()
	fragment = HTMLTableFragment(data, svcs.emptyQueryMeta)
	outputFile.write(nevowc.flattenSync(fragment))


formats.registerDataWriter("html", writeDataAsHTML, "text/html", "HTML",
	".html")
