# -*- coding: utf-8 -*-
"""
Renderers that take services "as arguments".
"""

#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 re
import urllib.request, urllib.parse, urllib.error

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

from gavo import base
from gavo import formal
from gavo import registry
from gavo import stc
from gavo import svcs
from gavo import utils
from gavo.web import common
from gavo.web import grend


def _protectForBibTeX(tx):
	"""returns tx in a way that hopefully prevents larger disasters
	when used with BibTeX.

	Among others, this just looks for multiple uppercase characters within
	one word and protects the respective word with curly braces; for now,
	this is ASCII only.

	This is also where we escape for TeX.
	"""
	return re.sub(r"([#$_&\\%])", r"\\\1",
		re.sub(r"(\w*[A-Z]\w+[A-Z]\w*)", r"{\1}", tx))


def makeBibTeXForMetaCarrier(mc):
	"""returns a (unicode) best-effort BibTeX record for something mixing in
	mc.

	This needs, at least, a creationDate, a creator, a referenceURL,
	and a title to work; if these aren't there, you'll get a MetaError.
	"""
	creationDate = utils.parseISODT(
		base.getMetaText(mc,
			"creationDate",
			raiseOnFail=True))
	year = creationDate.year
			
	# for the BibTeX tag, prefer the short name if given...
	label = base.getMetaText(mc, "shortName", None)
	if label is None:
		# ... else hope it's a table...
		try:
			label = mc.getQName()
		except AttributeError:
			# fall back to something made from the creation date
			# (which at least won't change between invocations)
			label = utils.intToFunnyWord(
				int(re.sub(r"[^\d]+", "", creationDate.isoformat())))
	label = "vo:"+re.sub(r"[^\w]", "_", label)

	authors = " and ".join(m.getContent()
				for m in mc.iterMeta("creator.name",
						propagate=True))

	res = [
		"@MISC{%s"%label,
		"  year=%s"%year,
		"  title={%s}"%_protectForBibTeX(
			base.getMetaText(mc, "title", "Untitled Resource")),
		"  author={%s}"%_protectForBibTeX(authors),
		"  url={%s}"%base.getMetaText(mc, "referenceURL", raiseOnFail=True),
		"  howpublished={{VO} resource provided by the %s}"%
			_protectForBibTeX(base.getConfig("web", "sitename")),
		]
	doi = base.getMetaText(mc, "doi", None)
	if doi:
		res.append(
			"  doi = {%s}"%doi)
	return ",\n".join(res)+"\n}\n"


class MetaRenderer(grend.CustomTemplateMixin, grend.ServiceBasedPage):
	"""Renderers that are allowed on all services.
	"""
	checkedRenderer = False

	@classmethod
	def isCacheable(self, segments, request):
		return True

	def data_otherServices(self, request, tag):
		"""returns a list of dicts describing other services provided by the
		the describing RD.

		The class mixing this in needs to provide a describingRD attribute for
		this to work.  This may be the same as self.service.rd, and the
		current service will be excluded from the list in this case.
		"""
		res = []
		for svc in self.describingRD.services:
			if svc is not self.service:
				res.append({"infoURL": svc.getURL("info"),
					"title": base.getMetaText(svc, "title")})
		return res

	@template.renderer
	def sortOrder(self, request, tag):
		if "alphaOrder" in request.strargs:
			return tag["Sorted alphabetically. ",
				T.a(href=request.path)[
					"[Sort by DB column index]"]]
		else:
			return tag["Sorted by DB column index. ",
				T.a(href=request.path+b"?alphaOrder=True")[
					"[Sort alphabetically]"]]

	@template.renderer
	def rdInfoLink(self, request, tag):
		# a link to the info to data's RD (i.e., data needs an rd attribute).
		return tag(href="/browse/"+tag.slotData.rd.sourceId)[
			RDInfoPage.makePageTitle(tag.slotData.rd)]

	@template.renderer
	def ifkey(self, keyName):
		def render(request, tag):
			if keyName in tag.slotData:
				return tag
			return ""
		return render

	@template.renderer
	def ifbibcode(self, request, tag):
		"""renders its children if the source metadata looks like a bibcode.
		"""
		source = base.getMetaText(self.metaCarrier, "source", "")
		if utils.couldBeABibcode(source):
			return tag
		else:
			return ""

	def data_bibtexentry(self, request, tag):
		"""returns BibTeX for the current record.

		This will return None if no BibTeX can be made.
		"""
		try:
			return makeBibTeXForMetaCarrier(self.metaCarrier)
		except base.MetaError:
			return None


class RendExplainer(object):
	"""is a container for various functions having to do with explaining
	renderers on services.

	Use the explain(renderer, service) class method.
	"""

	@classmethod
	def _explain_form(cls, service):
		return T.transparent["an interface for web browsers through an ",
			T.a(href=service.getURL("form"))["HTML form"]]

	@classmethod
	def _explain_fixed(cls, service):
		return T.transparent["a ",
			T.a(href=service.getURL("fixed"))["custom page"],
			", possibly with dynamic content"]

	@classmethod
	def _explain_volatile(cls, service):
		return T.transparent["a ",
			T.a(href=service.getURL("volatile"))["custom page"],
			", possibly with dynamic content"]

	@classmethod
	def _explain_soap(cls, service):

		def generateArguments():
			# Slightly obfuscated -- I need to get ", " in between the items.
			fieldIter = iter(service.getInputKeysFor("soap"))
			try:
				nextItem = next(fieldIter)
				while True:
					desc = "%s/%s"%(nextItem.name, nextItem.type)
					if nextItem.required:
						desc = T.strong[desc]
					yield desc
					nextItem = next(fieldIter)
					yield ', '
			except StopIteration:
				pass

		return T.transparent["enables remote procedure calls through the"
			" slightly aged SOAP mechanism; to use it,"
			" feed the WSDL URL "+
			service.getURL("soap")+"/go?wsdl"+
			" to your SOAP library; the function signature is"
			"  useService(",
			generateArguments(),
			").  See also our ",
			T.a(render="rootlink", href="/static/doc/soaplocal.shtml")[
				"local soap hints"]]

	@classmethod
	def _explain_custom(cls, service):
		res = T.transparent["a custom rendering of the service, typically"
			" for interactive web applications."]
		if svcs.getRenderer("custom").isBrowseable(service):
			res["  See also the ",
				T.a(href=service.getURL("custom"))["entry page"], "."]
		return res
	
	@classmethod
	def _explain_static(cls, service):
		return T.transparent["static (i.e. prepared) data or custom client-side"
			" code; probably used to access ancillary files here"]


	@classmethod
	def _explain_text(cls, service):
		return T.transparent["a text interface not intended for user"
			" applications"]

	@classmethod
	def _explain_siap_xml(cls, service):
		return T.transparent["a standard SIAP interface as defined by the"
			" IVOA to access collections of celestial images; SIAP clients"
			" use ", service.getURL("siap.xml"), " to access the service",
			T.transparent(render="ifadmin")[" – ",
				T.a(href="http://voparis-validator.obspm.fr/validator.php?"
					"spec=Simple+Image+Access+1.0"
					"&format=XHTML&batch=yes"
					+"&serviceURL=%s"%urllib.parse.quote(service.getURL("siap.xml"))
					+"&POS=%s%%2C%s&SIZE=%s%%2C%s&FORMAT=ALL"%(
						base.getMetaText(service, "testQuery.pos.ra", default="180"),
						base.getMetaText(service, "testQuery.pos.dec", default="60"),
						base.getMetaText(service, "testQuery.size.ra", default="3"),
						base.getMetaText(service, "testQuery.size.dec", default="3")))[
					"Validate"]]]

	@classmethod
	def _explain_siap2_xml(cls, service):
		return T.transparent["a standard SIAP version 2 interface as defined by the"
			" IVOA to access collections of celestial images; SIAP clients"
			" use ", service.getURL("siap2.xml"), " to access the service",
			T.transparent(render="ifadmin")[" – ",
				T.a(href="http://voparis-validator.obspm.fr/validator.php?"
					"spec=Simple+Image+Access+2.0"
					"&serviceURL=%s"%urllib.parse.quote(service.getURL("siap2.xml"))
					+"&FOV=&BAND=&TIME=&POL=&SPATRES=&EXPTIME=&ID=&COLLECTION=&FACILITY=&INSTRUMENT=&DPTYPE=image&CALIB=&TARGET=&TIMERES=&SPECRP=&FORMAT=&MAXREC=&format=XHTML&REQUEST=query"
					+"&POS=CIRCLE+{}+{}+{}".format(
						base.getMetaText(service, "testQuery.pos.ra", default="180"),
						base.getMetaText(service, "testQuery.pos.dec", default="60"),
						base.getMetaText(service, "testQuery.size.ra", default="3")))[
					"Validate"]]]

	@classmethod
	def _explain_scs_xml(cls, service):
		return T.transparent["a standard SCS interface as defined by the"
			" IVOA to access catalog-type data; SCS clients"
			" use ", service.getURL("scs.xml"), " to access the service",
			T.transparent(render="ifadmin")[" – ",
				T.a(href="http://voparis-validator.obspm.fr/validator.php?"
					"spec=Simple+Cone+Search+1.03"
					"&format=XHTML&batch=yes"
					+"&serviceURL=%s"%urllib.parse.quote(service.getURL("scs.xml"))
					+"&RA=%s&DEC=%s&SR=%s&VERB=3"%(
						base.getMetaText(service, "testQuery.ra", default="180"),
						base.getMetaText(service, "testQuery.dec", default="60"),
						base.getMetaText(service, "testQuery.sr", default="1")))[
					"Validate"]]]

	@classmethod
	def _explain_ssap_xml(cls, service):
		return T.transparent["a standard SSAP interface as defined by the"
			" IVOA to access spectral or time-series data; SSAP clients"
			" use ", service.getURL("ssap.xml"), " to access the service",
			T.transparent(render="ifadmin")[" – ",
				T.a(href="http://voparis-validator.obspm.fr/validator.php?"
					"REQUEST=queryData&POS=&SIZE=&TIME=&BAND=&FORMAT=ALL"
					"&VERSION=1.1&APERTURE=&SPECRP=&SPATRES=&TIMERES=&SNR="
					"&REDSHIFT=&VARAMPL=&TARGETNAME=&TARGETCLASS=&FLUXCALIB=any"
					"&WAVECALIB=any&PUBDID=&CREATORDID=&COLLECTION=&TOP=&MAXREC=1"
					"&MTIME=&COMPRESS=true&RUNID=&spec=Simple+Spectral+Access+1.1"
					"&format=XHTML"
					+"&serviceURL=%s"%urllib.parse.quote(service.getURL("ssap.xml")))[
				"Validate"]]]

	@classmethod
	def _explain_slap_xml(cls, service):
		return T.transparent["a standard SLAP interface as defined by the"
			" IVOA to access spectral line data; SLAP clients (usually"
			" spectral analysis programs)"
			" use ", service.getURL("slap.xml"), " to access the service",
			T.transparent(render="ifadmin")[" – ",
				T.a(href="http://voparis-validator.obspm.fr/validator.php?"
					"REQUEST=queryData&spec=Simple+Line+Access+1.0"
					"&format=XHTML"
					"&addparams=MAXREC%3D1%26"
					+"&serviceURL=%s"%urllib.parse.quote(service.getURL("slap.xml")))[
				"Validate"]]]

	@classmethod
	def _explain_dali(cls, service):
		return T.transparent["a complex interface consisting of multiple"
			" endpoints (sync, async, perhaps tables or examples, and possibly"
			" more).  This is used, in particular, for TAP.  You will usually need"
			" a client programme to use this."
			"  In such clients, you can find this service by its"
			" IVOID, ",
			T.code(render="meta")["identifier"],
			", or access it by entering its base URL ",
			T.code[service.getURL("dali")],
			" directly.  Using an XSL-enabled web browser you can, in a pinch,"
			" also operate ",
			T.a(href=service.getURL("async"))["the service"],
			" asynchronously without a specialized client."]

	@classmethod
	def _explain_uws_xml(cls, service):
		return T.transparent["a user-defined UWS service."
			" This service is best accessed using specialized clients"
			" or libraries. Give those its base URL ",
			T.a(href=service.getURL("uws.xml"))[service.getURL("uws.xml")],
			".  Using an XSL-enabled web browser you can"
			" also click on the link above and operate the service 'manually'."
			" For parameters and the output schema, see below."]

	@classmethod
	def _explain_pubreg_xml(cls, service):
		return T.transparent["an interface for the OAI-PMH protocol, typically"
			" this site's publishing registry (but possibly some other"
			" registry-like thing). This endpoint is usually accessed"
			" by harvesters, but with an XML-enabled browser you can"
			" also try the access URL at ",
			T.a(href=service.getURL("pubreg.xml"))[service.getURL("pubreg.xml")],
			"."]

	@classmethod
	def _explain_qp(cls, service):
		return T.transparent["an interface that uses the last path element"
			" to query the column %s in the underlying table."%
			service.getProperty("queryField", "defunct")]

	@classmethod
	def _explain_upload(cls, service):
		return T.transparent["a ",
			T.a(href=service.getURL("upload"))["form-based interface"],
			" for uploading data"]

	@classmethod
	def _explain_dlget(cls, service):
		return T.transparent["a datalink interface letting specialized clients"
			" retrieve parts of datasets or discover related data.  You"
			" use this kind of service exclusively in combination with"
			" a pubdid, usually via a direct link."]

	@classmethod
	def _explain_dlmeta(cls, service):
		return T.transparent["a datalink interface for discovering access"
			" options (processed data, related datasets...) for a dataset."
			" You usually need a publisherDID to use this kind of service."
			" For special applications, the base URL of this service might"
			" still come handy: %s"%service.getURL("dlmeta")]

	@classmethod
	def _explain_dlasync(cls, service):
		return T.transparent["an asynchronous interface to retrieving"
			" processed data.  This needs a special client that presumably"
			" would first look at the dlmeta endpoint to discover what"
			" processing options are available."]

	@classmethod
	def _explain_api(cls, service):
		return T.transparent["an interface for operation with curl and"
			" similar low-level tools.  The endpoint is at ",
			T.a(href=service.getURL("api"))[service.getURL("api")],
			"; as usual for DALI-conforming services, parameters"
			" an response structure is available by ",
			T.a(href=service.getURL("api")+"MAXREC=0")["querying with"
				" MAXREC=0"],
			"."]

	@classmethod
	def _explain_coverage(cls, service):
		return T.transparent["an interface to retrieve the spatial coverage"
			" of this service.  By default, this will return a FITS MOC,"
			" but browsers and similar clients declaring they accept PNGs"
			" will get a sky plot showing the coverage."]
	
	@classmethod
	def _explain_examples(cls, service):
		return T.transparent["a list of examples for"
			" how to use this resource.  You can read ",
			T.a(href=service.getURL("examples"))["the examples"],
			" with your browser.  RDFa-aware clients can figure out the"
			" examples at ",
			service.getURL("api")]

	@classmethod
	def _explain_hips(cls, service):
		return T.transparent["a HiPS endpoint for fast zooming and panning"
			" through in particular images.  Paste the access URL ",
			T.a(href=service.getURL("hips"))[service.getURL("hips")],
			" into a HiPS client or click it when you have a javascript-"
			"enabled browser."]

	@classmethod
	def _explain_biblinks_json(cls, service):
		return T.transparent["an endpoint for harvesting pairs of"
			" of bibliographic identifiers and dataset-related URLs."
			"  This is probably not interestering to you unless you"
			" are the ADS.  Retrieve the data from ",
			T.a(href=service.getURL("biblinks.json"))[
				service.getURL("biblinks.json")],
			"."]

	@classmethod
	def _explain_edition(cls, service):
		return T.transparent["a version of a document.  See ",
			T.a(href=service.getURL("edition"))[
				service.getURL("edition")],
			" for details."]

	@classmethod
	def _explainEverything(cls, service):
		return T.transparent["a renderer with some custom access method that"
			" should be mentioned in the service description"]

	@classmethod
	def explain(cls, renderer, service):
		return getattr(cls, "_explain_"+renderer.replace(".", "_"),
			cls._explainEverything)(service)


class ServiceInfoRenderer(MetaRenderer, utils.IdManagerMixin):
	"""A renderer showing all kinds of metadata on a service.

	This renderer produces the default referenceURL page.  To change its
	appearance, override the serviceinfo.html template.
	"""
	name = "info"
	
	customTemplate = svcs.loadSystemTemplate("serviceinfo.html")

	def __init__(self, *args, **kwargs):
		grend.ServiceBasedPage.__init__(self, *args, **kwargs)
		self.describingRD = self.service.rd
		self.rawFootnotes = set()

	@template.renderer
	def title(self, request, tag):
		return tag["Information on Service '%s'"%
			base.getMetaText(self.service, "title")]

	@template.renderer
	def notebubble(self, request, tag):
		if not tag.slotData["note"]:
			return ""
		id = tag.slotData["note"].tag
		self.rawFootnotes.add(tag.slotData["note"])
		return tag(href="#note-%s"%id)["Note %s"%id]

	@template.renderer
	def footnotes(self, request, tag):
		"""renders the footnotes as a definition list.
		"""
		return T.dl(class_="footnotes")[[
				T.xml(note.getContent(targetFormat="html"))
			for note in sorted(self.rawFootnotes, key=lambda n: n.tag)]]

	@template.renderer
	def spectral_coverage(self, request, tag):
		intervals = list(self.describingRD.iterMeta("coverage.spectral"))
		if not intervals:
			return ""

		tag["Intervals of messenger energies reflected in this resource: "]
		for m in intervals:
			lower, upper = map(float, m.getContent().split())
			# turn from Joule to eV
			lower = utils.formatFloat(lower/base.ELECTRON_CHARGE)
			upper = utils.formatFloat(upper/base.ELECTRON_CHARGE)
			tag[f"{lower} {upper} eV", T.br]
		return tag

	@staticmethod
	def _mjdToJYear(mjd):
		return utils.formatFloat(
				stc.dateTimeToJYear(
					stc.mjdToDateTime(float(mjd))))

	@template.renderer
	def temporal_coverage(self, request, tag):
		intervals = list(self.describingRD.iterMeta("coverage.temporal"))
		if not intervals:
			return ""

		tag["Time covered by this resource's data: "]
		for m in intervals:
			lower, upper = m.getContent().split()
			lower = self._mjdToJYear(lower)
			upper = self._mjdToJYear(upper)
			tag[f"{lower} {upper}", T.br]
		return tag

	def data_internalpath(self, request, tag):
		return "%s/%s"%(self.service.rd.sourceId, self.service.id)

	def data_inputFields(self, request, tag):
		res = [f.asInfoDict() for f in self.service.getInputKeysFor("info")]
		res.sort(key=lambda val: val["name"].lower())
		return res

	def data_htmlOutputFields(self, request, tag):
		res = [f.asInfoDict() for f in self.service.getCurOutputFields()]
		res.sort(key=lambda val: val["name"].lower())
		return res

	def data_votableOutputFields(self, request, tag):
		queryMeta = svcs.QueryMeta({"_FORMAT": "VOTable", "_VERB": 3})
		res = [f.asInfoDict() for f in self.service.getCurOutputFields(queryMeta)]
		res.sort(key=lambda val: val["verbLevel"])
		return res

	def data_rendAvail(self, request, tag):
		return [{"rendName": rend,
				"rendExpl": RendExplainer.explain(rend, self.service)}
			for rend in self.service.allowed]

	def data_publications(self, request, tag):
		res = [{"sets": ",".join(p.sets), "render": p.render}
			for p in self.service.publications
			if p.sets and p.render not in registry.HIDDEN_RENDERERS]
		return sorted(res, key=lambda v: v["render"])

	def data_browserURL(self, request, tag):
		return self.service.getBrowserURL()

	def data_service(self, request, tag):
		return self.service

	defaultLoader = common.doctypedStan(
		T.html[
			T.head[
				T.title["Missing Template"]],
			T.body[
				T.p["Infos are only available with a serviceinfo.html template"]]
		])


class TableInfoRenderer(MetaRenderer):
	"""A renderer for displaying table information.

	Since tables don't necessarily have associated services, this
	renderer cannot use a service to sit on.  Instead, the table is
	being passed in as as an argument. There's a built-in vanity tableinfo
	that sits on //dc_tables#show using this renderer (it could really
	sit anywhere else).
	"""
	name = "tableinfo"
	customTemplate = svcs.loadSystemTemplate("tableinfo.html")

	def render(self, request):
		if not hasattr(self, "table"):
			# _retrieveTableDef did not run, i.e., no tableName was given
			raise svcs.UnknownURI(
				"You must provide a table name to this renderer.")
		self.macroPackage = self.table
		self.metaCarrier = self.table
		return super(TableInfoRenderer, self).render(request)

	def _retrieveTableDef(self, tableName):
		try:
			self.tableName = tableName
			self.table = registry.getTableDef(tableName)
			self.describingRD = self.table.rd
		except base.NotFoundError as msg:
			raise base.ui.logOldExc(svcs.UnknownURI(str(msg)))

	def data_forADQL(self, request, tag):
		return self.table.adql

	def data_fields(self, request, tag):
		res = [f.asInfoDict() for f in self.table]
		for d in res:
			if d["note"]:
				d["noteKey"] = d["note"].tag
		if "alphaOrder" in request.strargs:
			res.sort(key=lambda item: item["name"].lower())
		return res

	def data_internalpath(self, request, tag):
		return "%s/%s"%(self.table.rd.sourceId, self.table.id)

	@template.renderer
	def title(self, request, tag):
		return tag["Table information for '%s'"%self.tableName]
	
	@template.renderer
	def rdmeta(self, request, tag):
		# rdmeta: Meta info at the table's rd (since there's ownmeta)
		metaKey = tag.children[0]
		tag.clear()
		htmlBuilder = common.HTMLMetaBuilder(self.describingRD)
		try:
			return tag[self.describingRD.buildRepr(metaKey, htmlBuilder)]
		except base.NoMetaKey:
			return ""

	@template.renderer
	def ifrdmeta(self, metaName):
		if self.describingRD.getMeta(metaName, propagate=False):
			return lambda request, tag: tag
		else:
			return lambda request, tag: ""

	@template.renderer
	def iftapinfo(self, request, tag):
		"""renders the content if there was a tapinfo key somewhere in
		the query string.
		"""
		if "tapinfo" in request.strargs:
			return tag
		else:
			return ""

	def data_tableDef(self, request, tag):
		return self.table

	def getChild(self, name, request):
		self._retrieveTableDef(name.decode("utf-8"))
		return self

	defaultLoader = common.doctypedStan(
		T.html[
			T.head[
				T.title["Missing Template"]],
			T.body[
				T.p["Infos are only available with a tableinfo.html template"]]
		])


class TableNoteRenderer(MetaRenderer):
	"""A renderer for displaying table notes.

	It takes a schema-qualified table name and a note tag in the segments.

	This does not use the underlying service, so it could and will run on
	any service.  However, you really should run it on __system__/dc_tables/show,
	and there's a built-in vanity name tablenote for this.
	"""
	name = "tablenote"

	def render(self, request):
		if not hasattr(self, "noteTag"):
			# _retrieveTableDef did not run, i.e., no tableName was given
			raise svcs.UnknownURI(
				"You must provide table name and note tag to this renderer.")
		return super(TableNoteRenderer, self).render(request)

	def _retrieveNote(self, tableName, noteTag):
		try:
			table = registry.getTableDef(tableName)
			self.metaCarrier = table
			self.renderedNote = table.getNote(noteTag
				).getContent(targetFormat="html", macroPackage=table)
		except base.NotFoundError as msg:
			raise base.ui.logOldExc(svcs.UnknownURI(msg))
		self.noteTag = noteTag
		self.tableName = tableName

	def getChild(self, name, request):
		segments = request.popSegments(name)

		if len(segments)==2:
			self._retrieveNote(segments[0], segments[1])
		elif len(segments)==3: # segments[0] may be anything,
			# but conventionally "inner"
			self._retrieveNote(segments[1], segments[2])
			self.loader = self.innerLoader
		else:
			raise svcs.UnknownURI("No such table note")
		return self

	def data_tableName(self, request, tag):
		return self.tableName
	
	def data_noteTag(self, request, tag):
		return self.noteTag
	
	@template.renderer
	def noteHTML(self, request, tag):
		return T.xml(self.renderedNote)

	loader = formal.XMLString(r"""
<html xmlns="xmlns=http://www.w3.org/1999/xhtml"
	xmlns:n="http://nevow.com/ns/nevow/0.1">
<head>
	<title>\getConfig{web}{sitename} – Note for table
		<n:invisible n:render="unicode" n:data="tableName"/></title>
	<n:invisible n:render="commonhead"/>
	<style>
		span.target {font-size: 180%;font-weight:bold}
	</style>
</head>
<body>
	<n:invisible n:render="noteHTML"/>
</body>
</html>""")

	innerLoader = template.TagLoader(
		T.transparent(render="noteHTML"))


class HowToCiteRenderer(MetaRenderer):
	"""A renderer that lets you format citation instructions.
	"""
	name = "howtocite"

	customTemplate = svcs.loadSystemTemplate("howtocite.html")


class CoverageRenderer(MetaRenderer):
	"""A renderer returning various forms of a service's spatial coverage.

	This will return a 404 if the service doesn't have a coverage.spatial
	meta (and will bomb out if that isn't a SMoc).

	Based on the accept header, it will return a PNG if the client
	indicates it's interested in that or if it accepts text/html, in which
	case we assume it's a browser; otherwise, it will produce a
	MOC in FITS format.
	"""
	name = "coverage"

	def render(self, request):
		from gavo.utils import pgsphere
		try:
			moc = pgsphere.SMoc.fromASCII(
				self.service.getMeta(
					"coverage.spatial", raiseOnFail=True).getContent())
		except base.NoMetaKey:
			raise svcs.UnknownURI(
				"No spatial coverage available for this service.")

		if self.returnAPNG(request):
			request.setHeader("content-type", "image/png")
			return moc.getPlot(xsize=400)

		else:
			request.setHeader("content-type", "application/fits")
			return moc.asFITS()

	@classmethod
	def returnAPNG(self, request):
		"""returns true if request indicates we're being retrieved by
		a web browser.
		"""
		acceptDict = utils.parseAccept(request.getHeader("accept"))
		return ("image/png" in acceptDict
				or "image/webp" in acceptDict # firefox ~78 hack
				or "image/*" in acceptDict
				or "text/html" in acceptDict)

	@classmethod
	def isCacheable(cls, segments, request):
		"""only cache this if we return a PNG.

		Our caching system doesn't support content negotiation, so we
		can't keep the FITS (and it's fast to generate, so that doesn't
		matter so much.
		"""
		return cls.returnAPNG(request)


class EditionRenderer(MetaRenderer):
	"""A renderer representing a (tutorial-like) text document.

	This must have a meta accessURL with the document URI.  It may
	have a sourceURL meta giving the VCS URI.
	"""
	name = "edition"

	@template.renderer
	def title(self, request, tag):
		return tag["Editions of “{}”".format(
			base.getMetaText(self.metaCarrier, "title", default="unnamed document"))]
	
	@template.renderer
	def datameta(self, request, tag):
		return self._doRenderMeta(request, tag, carrier=tag.slotData)

	@template.renderer
	def ifdatameta(self, metaName):
		def _(request, tag):
			if tag.slotData.getMeta(metaName) is not None:
				return tag
			else:
				return ""
		return _

	@template.renderer
	def accurl(self, request, tag):
		url = base.getMetaText(tag.slotData, "accessURL")
		return tag(href=url, class_="access-url")[url]

	def data_editions(self, request, tag):
		return self.metaCarrier.publications or []

	defaultLoader =  common.doctypedStan(
		T.html[
			T.head[
				T.title(render="title"),
				T.transparent(render="commonhead"),
			],
			T.body(render="withsidebar")[
				T.h1(render="title"),
				T.p["The following editions of this document are available:"],
				formal.addNevowAttributes(T.ul, data="editions")(render="sequence")[
					formal.addNevowAttributes(T.li, pattern="item")[
						T.a(render="accurl"),
						T.transparent(render="ifdatameta locTitle") [
							" with a localised title ",
							T.em(render="datameta")["locTitle"]],
						T.transparent(render="ifdatameta languageCode")[
							"  written in the BCP 47 language ",
							T.em(class_="langcode", render="datameta")["languageCode"]],
					".",
					T.transparent(render="ifdatameta sourceURL") [
						"  Source code available at ",
						T.transparent(render="datameta")["sourceURL"]]]
		]]])


class ExternalRenderer(grend.ServiceBasedPage):
	"""A renderer redirecting to an external resource.

	These try to access an external publication on the parent service
	and ask it for an accessURL.  If it doesn't define one, this will
	lead to a redirect loop.

	In the DC, external renderers are mainly used for registration of
	third-party browser-based services.
	"""
	name = "external"

	@classmethod
	def getExternalURL(cls, service):
		"""returns the URL this service should redirect to.
		"""
		# look for a matching publication in the parent service...
		for pub in service.publications:
			if pub.render==cls.name:
				break
		else: # no publication, 404
			raise svcs.UnknownURI("No publication for an external service here.")
		return base.getMetaText(pub, "accessURL", macroPackage=service)

	@classmethod
	def isBrowseable(self, service):
		try:
			destSchema = self.getExternalURL(service).split(":")[0]
			return destSchema in ("http", "https")
		except Exception:
			return False

	def render(self, request):
		raise svcs.WebRedirect(self.getExternalURL(self.service))


class VOEventRenderer(ExternalRenderer):
	"""A purely formal renderer only used for registering VOEventStreams.

	VOEvent registration is done in a way that the individual interfaces
	carry standard ids and would be the carriers of the actual protocols.
	But we'll probably never have any of that in DaCHS, so actually,
	this is only being used to link a renderer name and a standard id.
	"""
	standardId = "ivo://ivoa.net/std/voevent"
	name = "voevent"


class RDInfoPage(grend.CustomTemplateMixin, grend.ResourceBasedPage):
	"""A page giving infos about an RD.

	This is not a renderer but a helper for RDInfoRenderer.
	"""
	customTemplate = svcs.loadSystemTemplate("rdinfo.html")

	def data_services(self, request, tag):
		return sorted(self.rd.services,
			key=lambda s: base.getMetaText(s, "title", default=s.id))
	
	def data_tables(self, request, tag):
		return sorted((t for t in self.rd.tables
			if t.onDisk
				and not t.temporary
				and not t.hasProperty("internal")),
			key=lambda t: t.id)

	def data_otherrecs(self, request, tag):
		return self.rd.resRecs

	def data_clientRdId(self, request, tag):
		return self.rd.sourceId

	def _getDescriptionHTML(self, descItem):
		"""returns stan for the "description" of a service or a table.

		The RD's description is not picked up.
		"""
		iDesc = descItem.getMeta("description", propagate=False)
		if iDesc is None:
			return ""
		else:
			return T.div(class_="lidescription")[
				T.xml(iDesc.getContent("blockhtml", macroPackage=descItem))]

	@template.renderer
	def rdsvc(self, request, tag):
		service = tag.slotData
		return tag[
			T.a(href=service.getURL("info"))[
				base.getMetaText(service, "title", default=service.id)],
				self._getDescriptionHTML(service)]
			
	@template.renderer
	def rdtable(self, request, tag):
		tableDef = tag.slotData
		qName = tableDef.getQName()

		adqlNote = ""
		if tableDef.adql:
			adqlNote = T.span(class_="adqlnote")[" ",
				"–", " queryable through ",
				T.a(href="/tap")["TAP"], " and ",
				T.a(href="/adql")["ADQL"],
				" "]

		return tag[
			T.a(href="/tableinfo/%s"%qName)[qName],
			adqlNote,
			self._getDescriptionHTML(tableDef)]

	@classmethod
	def makePageTitle(cls, rd):
		"""returns a suitable title for the rd info page.

		This is a class method to allow other renderers to generate
		titles for link anchors.
		"""
		return "Information on resource '%s'"%base.getMetaText(
			rd, "title", default="%s"%rd.sourceId)

	@template.renderer
	def title(self, request, tag):
		return tag[self.makePageTitle(self.rd)]

	defaultLoader =  common.doctypedStan(
		T.html[
			T.head[
				T.title["Missing Template"]],
			T.body[
				T.p["RD infos are only available with an rdinfo.html template"]]
		])


class RDInfoRenderer(grend.CustomTemplateMixin, grend.ServiceBasedPage):
	"""A renderer for displaying various properties about a resource descriptor.
	
	This renderer could really be attached to any service since
	it does not call it, but it usually lives on //services/overview.

	By virtue of builtin vanity, you can reach the rdinfo renderer
	at /browse, and thus you can access /browse/foo/q to view the RD infos.
	This is the form used by table registrations.

	In addition to all services, this renderer also links tableinfos
	for all non-temporary, on-disk tables defined in the RD.  When
	you actually want to hide some internal on-disk tables, you can
	set a property ``internal`` on the table (the value is ignored).
	"""
	name = "rdinfo"
	customTemplate = svcs.loadSystemTemplate("rdlist.html")

	def data_publishedRDs(self, request, tag):
		with base.getTableConn() as conn:
			return [row[0] for row in
				conn.query(
					"""SELECT DISTINCT sourceRD
					FROM (
						SELECT sourceRD FROM dc.resources
						WHERE NOT deleted) as q
					ORDER BY sourceRD""")]

	def getChild(self, name, request):
		rdId = "/".join(request.popSegments(name))
		if not rdId:
			raise svcs.WebRedirect("browse")
		clientRD = base.caches.getRD(rdId)
		return RDInfoPage(request, clientRD)

	defaultLoader =  common.doctypedStan(
		T.html[
			T.head[
				T.title["Missing Template"]],
			T.body[
				T.p["The RD list is only available with an rdlist.html template"]]
		])


class ResourceRecordMaker(resource.Resource):
	"""A page that returns resource records for internal services.

	This is basically like OAI-PMH getRecord, except we're using rd/id/svcid
	from our path.

	Also (and that's fairly important for purx), this will use the
	_metadataUpdated meta for a modified header.
	"""
	def render(self, request):
		raise svcs.UnknownURI("What resource record do you want?")

	def getChild(self, name, request):
		from gavo.registry import builders
		segments = request.popSegments(name)

		rdParts, svcId = segments[:-1], segments[-1]
		rdId = "/".join(rdParts)
		try:
			rd = base.caches.getRD(rdId)
			resob = rd.getById(svcId)
		except base.NotFoundError:
			raise svcs.UnknownURI("The resource %s#%s is unknown at this site."%(
				rdId, svcId))

		timestampUpdated = utils.parseISODT(
			resob.getMeta("_metadataUpdated").getContent("text")).timestamp()

		return common.TypedData(
			utils.xmlrender(
				builders.getVORMetadataElement(resob),
				prolog="<?xml version='1.0'?>"
					"<?xml-stylesheet href='/static/xsl/oai.xsl' type='text/xsl'?>",
				),
			"application/xml",
			timestampUpdated)
