"""
Typed metadata.

These are subclasses of base.MetaValue that deal with special sorts
of metadata.  Most of the time, they organise some extra (hidden or open)
keys, or they do some special HTML rendering.

Use the meta.forKeys class decorator to teach meta.py where to
use your new class.
"""

#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 datetime
import re
from urllib import parse as urlparse

from gavo import utils
from gavo.base import config
from gavo.base import meta
from gavo.utils import misctricks
from gavo.utils import stanxml

from gavo.utils.dachstypes import (
	cast, Any, Optional, Sequence, Union)


def _getMSContent(ms: meta._MetaSeed) -> str:
	"""returns the stringified version of a _MetaSeed.

	This probably should go to base.meta when we make _MetaSeed public.
	For now, let's pretend the two modules are good friends.
	"""
	if ms is None:
		return ""
	elif isinstance(ms, meta.MetaValue):
		return ms.content
	else:
		return cast(str, ms)


@meta.forKeys("_related", "referenceURL")
class MetaURL(meta.MetaValue):
	"""A meta value containing a link and optionally a title

	In plain text, this would look like
	this::
	
	  _related:http://foo.bar
	  _related.title: The foo page

	In XML, you can write::

	  <meta name="_related" title="The foo page"
	    ivoId="ivo://bar.org/foo">http://foo.bar</meta>

	or, if you prefer::

	  <meta name="_related">http://foo.bar
	    <meta name="title">The foo page</meta></meta>
	  </meta>
	
	_related meta is rendered in the sidebar in the web interface; it is
	currently ignored for relationships published to the VO.

	For links within your DaCHS site, use the internallink macro, the argument
	of which the the "path" to a resource, i.e. RD path/service/renderer;
	we recommend to use the info renderer in such links as a rule.  This would
	look like this::
		
	  meta name="_related" title="Aspec SSAP"
	    \\internallink{aspec/q/ssa/info}</meta>
	"""
	def __init__(self,
			url: str,
			format: str = "plain",
			title: Optional[str] = None) -> None:
		if url:
			url = url.strip()
		meta.MetaValue.__init__(self, url, format)
		self.title = title

	def _getContentAsHTML(self, content: str, block: bool = False) -> str:
		title = self.title or content
		return '<a href=%s>%s</a>'%(
			stanxml.escapeAttrVal(content), stanxml.escapePCDATA(title))

	def _addMeta(self, atoms: Sequence[str], metaValue: meta._MetaSeed) -> None:
		if atoms[0]=="title":
			self.title = _getMSContent(metaValue)
		else:
			meta.MetaValue._addMeta(self, atoms, metaValue)


@meta.forKeys(
# if you add new RelationResourceMeta meta keys, be you'll also need to
# amend registry.builders._vrResourceBuilder

# VOResource 1.0 terms
	"servedBy",
	"serviceFor",
	"relatedTo",
	"mirrorOf",
	"derivedFrom",
	"uses",

# VOResource 1.1 terms
	"cites",
	"isSupplementTo",
	"isSupplementedBy",
	"isContinuedBy",
	"continues",
	"isNewVersionOf",
	"isPreviousVersionOf",
	"isPartOf",
	"hasPart",
	"isSourceOf",
	"isDerivedFrom",
	"isIdenticalTo",
	"isServiceFor",
	"isServedBy")
class RelatedResourceMeta(meta.MetaValue):
	"""A meta value containing an ivo-id and a name of a related resource.

	The sort of relationsip is encoded in the meta name, where the terms
	are defined in the vocabuary
	http://www.g-vo.org/rdf/voresource/relationship_type (where there
	are minor lexical deviations from identifiers there to DaCHS' meta names).

	All relationship metas should look like this (using isSupplementTo as an
	example; while an ivoId is not mandatory, it rarely makes sense to declare
	a relationship without it)::

	  isSupplementTo: GAVO TAP service
	  isSupplementTo.ivoId: ivo://org.gavo.dc

	It is also possible to include an altIdentifier (such as a DOI) to declare
	a relationship to a resource not declared in the IVOA registry.

	``isServedBy`` and ``isServiceFor`` are somewhat special cases, as
	the service attribute of data publications automatically takes care
	of them; so, you shouldn't usually need to bother with these two manually.
	"""
	def __init__(self,
			title: str,
			format: str = "plain",
			ivoId: Optional[str] = None,
			altIdentifier: Optional[str] = None) -> None:
		meta.MetaValue.__init__(self, title, format)
		if ivoId is not None:
			self._addMeta(["ivoId"], meta.MetaValue(ivoId))
		if altIdentifier is not None:
			self._addMeta(["altIdentifier"], meta.MetaValue(altIdentifier))
	

@meta.forKeys("_news")
class NewsMeta(meta.MetaValue):
	"""A meta value representing a "news" item.

	The content is the body of the news.  In addition, they have
	date, author, and role children.  In plain text, you would write::

	  _news: Frobnicated the quux.
	  _news.author: MD
	  _news.date: 2009-03-06
	  _news.role: updated
	
	In XML, you would usually write::

	  <meta name="_news" author="MD" date="2009-03-06">
	    Frobnicated the quux.
	  </meta>
	
	_news items become serialised into Registry records despite their
	leading underscores.  role then becomes the date's role.  	
	"""
	discardChildrenInHTML = True

	def __init__(self,
			content: str,
			format: str = "plain",
			author: Optional[str] = None,
			date: Optional[str] = None,
			role: Optional[str] = None) -> None:
		meta.MetaValue.__init__(self, content, format)
		self.initArgs = format, author, date, role
		for key in ["author", "date", "role"]:
			val = locals()[key]
			if val is not None:
				self._addMeta([key], meta.MetaValue(val))

	def _getContentAsHTML(self, content: str, block: bool = False) -> str:
		authorpart = ""
		if self.author:
			authorpart = " (%s)"%self.author
		return meta.IncludesChildren('<span class="newsitem">%s%s: %s</span>'%(
			stanxml.escapePCDATA(self.date),
			stanxml.escapePCDATA(authorpart),
			meta.MetaValue._getContentAsHTML(self, content, block)))
	
	def _addMeta(self, atoms: Sequence[str], metaValue: meta._MetaSeed) -> None:
		if atoms[0]=="author":
			self.author = _getMSContent(metaValue)
		elif atoms[0]=="date":
			self.date = _getMSContent(metaValue)
		elif atoms[0]=="role":
			self.role = _getMSContent(metaValue)
		meta.MetaValue._addMeta(self, atoms, metaValue)


@meta.forKeys("note")
class NoteMeta(meta.MetaValue):
	"""A meta value representing a "note" item.

	This is like a footnote, typically on tables, and is rendered in table
	infos.

	The content is the note body.  In addition, you want a tag child that
	gives whatever the note is references as.  We recommend numbers.

	Contrary to other meta items, note content defaults to rstx format.

	Typically, this works with a column's note attribute.

	In XML, you would usually write::

	  <meta name="note" tag="1">
	    Better ignore this.
	  </meta>
	"""
	def __init__(self,
			content: str,
			format: str = "rst",
			tag: str = "not-set") -> None:
		meta.MetaValue.__init__(self, content, format)
		self.initArgs = content, format, tag
		self.tag = tag or "(untagged)"

	def _getContentAsHTML(self, content: str, block: bool = False) -> str:
		return ('<dt class="notehead">'
				'<a name=%s>Note %s</a></dt><dd>%s</dd>')%(
			stanxml.escapeAttrVal("note-"+self.tag),
			stanxml.escapePCDATA(self.tag),
			meta.MetaValue._getContentAsHTML(self, content))
	
	def _addMeta(self, atoms: Sequence[str], metaValue: meta._MetaSeed) -> None:
		if atoms[0]=="tag":
			self.tag = _getMSContent(metaValue)
		else:
			meta.MetaValue._addMeta(self, atoms, metaValue)


@meta.forKeys("creationDate", "_dataUpdated", "_metadataUpdated")
class DatetimeMeta(meta.MetaValue):
	"""A meta value representing a timestamp.

	Accessing it, you will get a formatted ISO/DALI string.  You
	can construct them with both strings (that we'll try to parse and
	bomb if that's not possible) and datetime.datetime objects.
	"""
	def __init__(self,
			content: Union[datetime.datetime, str],
			*args: str,
			**kwargs: Any) -> None:
		if isinstance(content, str):
			if content.startswith("\\metaString{authority"):
				# backwards compatibility hack: legacy (1.0) userconfigs would try
				# to pull authority.creationDate via unexpanded macros, and
				# that breaks now.  Hack around it for now.
				content = meta.getMetaText(meta.CONFIG_META, "authority.creationDate")

			content = utils.parseISODT(content)

		if isinstance(content, datetime.datetime):
			content = utils.formatISODT(content)

		meta.MetaValue.__init__(self, content, *args, **kwargs)


@meta.forKeys("info")
class InfoItem(meta.MetaValue):
	"""A meta value for info items in VOTables.

	In addition to the content (which should be rendered as the info element's
	text content), it contains an infoName and an infoValue.
	
	They are only used internally in VOTable generation and might go away
	without notice.
	"""
	def __init__(self,
			content: str,
			format: str = "plain",
			infoName: Optional[str] = None,
			infoValue: Optional[str] = None,
			infoId: Optional[str] = None) -> None:
		meta.MetaValue.__init__(self, content, format)
		self.initArgs = content, format, infoName, infoValue, infoId
		self.infoName, self.infoValue = infoName, infoValue
		self.infoId = infoId


@meta.forKeys("logo", "creator.logo")
class LogoMeta(meta.MetaValue):
	"""A MetaValue corresponding to a small image.

	These are rendered as little images in HTML.  In XML meta, you can
	say::

	  <meta name="_somelogo" type="logo">http://foo.bar/quux.png</meta>
	"""
	def __init__(self,
			content: str = "",
			*args: str,
			**kwargs: Any) -> None:
		if not "\\" in content and not re.match("https?://", content):
			# see above on avoiding circular imports
			content = urlparse.urljoin(
				config.get("web", "serverURL"),
				content)
		meta.MetaValue.__init__(self, content, *args, **kwargs)

	def _getContentAsHTML(self, content: str, block: bool = False) -> str:
		return '<img class="metalogo" src="%s" alt="[Logo]"/>'%(
				str(content).strip())


@meta.forKeys("source")
class BibcodeMeta(meta.MetaValue):
	"""A MetaValue that may contain bibcodes, which are rendered as links
	into ADS.
	"""
	def _makeADSLink(self, matOb: re.Match) -> str:
		urlBibcode = urlparse.quote(matOb.group(0))
		adsURL = f"https://ui.adsabs.harvard.edu/abs/{urlBibcode}/abstract"
		return '<a href="{}">{}</a>'.format(
			adsURL,
			stanxml.escapePCDATA(matOb.group(0)))

	def _getContentAsHTML(self, content: str, block: bool = False) -> str:
		content = str(content)
		mat = misctricks.BIBCODE_PATTERN.match(content)
		if mat:
			return self._makeADSLink(mat)
		else:
			return stanxml.escapePCDATA(content)


@meta.forKeys("votlink")
class VotLinkMeta(meta.MetaValue):
	"""A MetaValue serialized into VOTable links (or, ideally,
	analogous constructs).

	This exposes the various attributes of VOTable LINKs as href
	linkname, contentType, and role.  You cannot set ID here; if this ever
	needs referencing, we'll need to think about it again.	
	The href attribute is simply the content of our meta (since
	there's no link without href), and there's never any content
	in VOTable LINKs).

	You could thus say::

	  votlink: http://docs.g-vo.org/DaCHS
	  votlink.role: doc
	  votlink.contentType: text/html
	  votlink.linkname: GAVO DaCHS documentation
	"""
	def __init__(self,
			href: str,
			format: str = "plain",
			linkname: Optional[str] = None,
			contentType: Optional[str] = None,
			role: Optional[str] = None) -> None:
		meta.MetaValue.__init__(self, href, format)
		for key in ["linkname", "contentType", "role"]:
			val = locals()[key]
			if val is not None:
				self._addMeta([key], meta.MetaValue(val))


@meta.forKeys("_example")
class ExampleMeta(meta.MetaValue):
	"""A MetaValue to keep VOSI examples in.

	All of these must have a title, which is also used to generate
	references.

	These also are in reStructuredText by default, and changing
	that probably makes no sense at all, as these will always need
	interpreted text roles for proper markup.

	Thus, the usual pattern here is::

	  <meta name="_example" title="An example for _example">
	    See docs_

	    .. _docs: http://docs.g-vo.org
	  </meta>
	"""
	def __init__(self,
			content: str,
			format: str = "rst",
			title: str = "Unnamed Example") -> None:
		if title=="Unnamed Example":
			raise meta.MetaError("_example meta must always have a title")
		meta.MetaValue.__init__(self, content, format)
		self._addMeta(["title"], meta.MetaValue(title))


@meta.forKeys("doi")
class DOIMeta(meta.MetaValue):
	"""A MetaValue for a DOI.

	This lets people construct DOI meta with or without a doi: prefix.
	It also creates landing page links in HTML.
	"""
	def __init__(self, content: str, **kwargs: Any) -> None:
		if content:
			if content.startswith("doi:"):
				content = content[4:]
			if not re.match("[0-9.]+/", content):
				raise meta.MetaValueError("%s does not look like a DOI"%content)
		meta.MetaValue.__init__(self, content.strip(), **kwargs)
	
	def _getContentAsHTML(self, content: str, block: bool = False) -> str:
		return '<a href=%s>%s</a>'%(
			stanxml.escapeAttrVal("http://dx.doi.org/"+content),
			stanxml.escapePCDATA(content))


# subject meta is in protocols.vocabularies
