"""
Code dealing with meta information.

In DaCHS, tree-structured metadata can be attached to tables, services,
resources as a whole, and some other structures.  This module provides
a python mixin to keep and manipulate such metadata.

We deal with VO-style RMI metadata but also allow custom metadata.  Custom
metadata keys should usually start with _.

See develNotes for some discussion of why this is so messy and an explanation
of why in particular addMeta and helpers are a minefield of selections.

The rough plan to do this looks like this:

Metadata is kept in containers that mix in MetaMixin.  Meta information is
accessed through keys of the form <atom>{.<atom>}  The first atom is the
primary key.  An atom consists exclusively of ascii letters and the
underscore.

There's a meta mixin having the methods addMeta and getMeta that care
about finding metadata including handover.  For compound metadata, they
will split the key and hand over to the parent if the don't have a meta
item for the main key.

To make this complete for DaCHS, this needs to be completemented with magic,
"typed" meta classes overriding certain types of behaviour (see the
forKeys class decorator for how this works); base.__init__ arranges
for base.typedmeta to be imported.  To access these magic types,
use META_CLASSES_FOR_KEYS (but don't change that directory except
through @meta.forKeys).
"""

#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 functools
import re
import textwrap

from gavo import utils
from gavo.base import attrdef
from gavo.base import common
from gavo.base import config
from gavo.utils import stanxml

from gavo.utils.dachstypes import (
	cast,
	Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence,
	StanElement, StructParseContext, StructParserValue,
	Structure, Tuple, TYPE_CHECKING, Type, Union)


if TYPE_CHECKING:
	# what you can pass as value to most methods
	_MetaSeed = Union[None, str, "MetaMixin"]
	from gavo.base.macros import MacroPackage
else:
	_MetaSeed, MacroPackage = None, None


# This dictionary maps meta atoms that were found to be somehow
# misleading or confusing to the ones that are now used.  This
# is so RDs won't get broken.
DEPRECATED_ATOMS = {
	"_associatedDatalinkSvc": "_associatedDatalinkService",
}


class MetaError(utils.Error):
	"""A base class for metadata-related errors.

	MetaErrors have a carrier attribute that should point to the MetaMixin
	item queried.  Metadata propagation makes this a bit tricky, but we
	should at least try; for setMeta and addMeta, the top-level entry
	functions manipulate the carrier attributes for this purpose.

	To yield useful error messages, leave carrier at its default None
	only when you really have no idea what the meta will end up on.
	"""
	def __init__(self,
			msg: str,
			carrier: Optional["MetaMixin"] = None,
			hint: Optional[str] = None,
			key: Optional[str] = None) -> None:
		self.carrier = carrier
		self.key = key
		utils.Error.__init__(self, msg, hint=hint)

	def __str__(self) -> str:
		prefix = ""
		if self.carrier:
			prefix = "On %s: "%repr(self.carrier)
		return prefix+utils.Error.__str__(self)

class MetaSyntaxError(MetaError):
	"""is raised when a meta key is invalid.
	"""


class NoMetaKey(MetaError):
	"""is raised when a meta key does not exist (and raiseOnFail is True).
	"""


class MetaCardError(MetaError):
	"""is raised when a meta value somehow has the wrong cardinality (e.g.,
	on attempting to stringify a sequence meta).
	"""
	def __init__(self,
			msg: str,
			carrier: Optional["MetaMixin"] = None,
			hint: Optional[str] = None,
			key: Optional[str] = None) -> None:
		self.origMsg = msg
		if key is None:
			MetaError.__init__(self, msg, hint=hint)
		else:
			MetaError.__init__(self, "%s (key %s)"%(msg, key), hint=hint)


class MetaValueError(MetaError):
	"""is raised when a meta value is inapproriate for the key given.
	"""

#############################################
# Magic meta handling start

META_CLASSES_FOR_KEYS: Dict[str, Type["MetaValue"]] = {}

def forKeys(*keys: str) -> Callable[[Type["MetaValue"]], Type["MetaValue"]]:
	"""decorates typed MetaValues and declares them to handle one or more
	meta keys.
	"""
	def _(cls: Type["MetaValue"]) -> Type["MetaValue"]:
		for k in keys:
			assert k not in META_CLASSES_FOR_KEYS, "Redefining "+k
			META_CLASSES_FOR_KEYS[k] = cls
		return cls
	return _


@functools.lru_cache(maxsize=16)
def getKeysForClass(metaCls: Type["MetaValue"]) -> Sequence[str]:
	"""yields all the keys that will lead to a metaCls-typed meta value.

	subclasses count as well.
	"""
	return [key
		for key, val in META_CLASSES_FOR_KEYS.items()
		if issubclass(val, metaCls)]


def doMetaOverride(
		container: "MetaMixin",
		metaKey: str,
		metaValue: _MetaSeed,
		extraArgs: Dict[str, Any] = {}) -> bool:
	"""creates the representation of metaKey/metaValue in container.

	This is there for magic meta types, mostly from typedmeta.  It
	inspects META_CLASSES_FOR_KEYS, which in turn is fed by
	the forKeys class decorator.  There's some hard-coded junk
	in here that doesn't quite fit anywhere else yet (but that should
	be rationalised if more hackery of this kind becomes necessary).

	If metaKey does not need any special action, this returns None.

	This gets called from one central point in MetaMixin.addMeta, and
	essentially all magic involved should be concentrated here.
	"""
	if metaKey in META_CLASSES_FOR_KEYS and not isinstance(metaValue, MetaValue):
		try:
			container.addMeta(metaKey,
				META_CLASSES_FOR_KEYS[metaKey](metaValue, **extraArgs))
			return True
		except TypeError:
			raise utils.logOldExc(MetaError(
				"Invalid arguments for %s meta items: %s"%(metaKey,
					utils.safe_str(extraArgs)), None))

	# let's see if there's some way to rationalise this kind of thing
	# later.
	if metaKey=="creator":
		return _doCreatorMetaOverride(container, metaValue)
	
	elif metaKey=="copyright" or metaKey=="_copyright":
		# Backwards compatibility 2019; it's cheap enough so I don't
		# bother deprecating the two keys for now.
		container.addMeta("rights", metaValue, **extraArgs)

	# fallthrough: let addMeta do its standard thing.
	return False


def _doCreatorMetaOverride(
		container: "MetaMixin",
		value: _MetaSeed) -> bool:
	"""handles the adding of the creator meta.

	value is empty or a parsed meta value, this does nothing (which will cause
	addMeta to do its default operation).

	If value is a non-empty string, it will be split along semicolons
	to produce individual creator metas with names .
	"""
	if not value or isinstance(value, MetaValue):
		return False

	value = cast(str, value)
	for authName in (s.strip() for s in value.split(";")):
		container.addMeta("creator",
		 MetaValue().addMeta("name", authName))
	
	return True


class IncludesChildren(str):
	"""a formatted result that already includes all meta children.

	This is returned from some of the special meta types' HTML formatters
	to keep the HTMLMetaBuilder from adding meta items that are already
	included in their HTML.
	"""

# Magic meta handling end
#############################################


_metaPat = re.compile(r"([a-zA-Z_][\w-]*)(?:\.([a-zA-Z_][\w-]*))*$")
_primaryPat = re.compile(r"([a-zA-Z_][\w-]*)(\.|$)")


def parseKey(metaKey: str) -> Sequence[str]:
	"""returns a list of the meta atoms in a meta key.

	Deprecated atoms are replaced by their successors.
	"""
	if not _metaPat.match(metaKey):
		raise MetaSyntaxError("Invalid meta key: %s"%metaKey, None)

	parsed = metaKey.split(".")
	for atom in parsed:
		if atom in DEPRECATED_ATOMS:
			utils.sendUIEvent("Warning", "Deprecated meta atom %s used in %s;"
				"  please change the atom to %s."%(
					atom, metaKey, DEPRECATED_ATOMS[atom]))
		return [DEPRECATED_ATOMS.get(a, a) for a in parsed]

	return parsed


def parseMetaStream(
		metaContainer: "MetaMixin",
		metaStream: str,
		clearItems: bool = False) -> None:
	"""parses meta key/value pairs from metaStream and adds them to
	metaContainer.

	If clearItems is true, for each key found in the metaStream there's
	first a delMeta for that key executed.  This is for re-parsing
	meta streams.

	The stream format is:
	
	 - continuation lines with backslashes, where any sequence of
	   backslash, (cr?) lf (blank or tab)* is replaced by nothing.
	 - comments are lines like (ws*)# anything
	 - empty lines are no-ops
	 - all other lines are (ws*)<key>(ws*):(ws*)value(ws*)
	 - if a key starts with !, any meta info for the key is cleared before
	   setting
	 - a value may be prefixed with rst:, literal:, or raw: to set the format
	   of the meta literal (default is plain).
	"""
	if metaStream is None:
		return

	# handle continuation lines
	metaStream = re.sub("\\\\\r?\n[\t ]*", "", metaStream)

	keysSeen = set()

	for line in metaStream.split("\n"):
		line = line.strip()
		if line.startswith("#") or not line:
			continue
		try:
			key, value = line.split(":", 1)
		except ValueError:
			raise MetaSyntaxError("%s is no valid line for a meta stream"%
				repr(line), None,
				hint="In general, meta streams contain lines like 'meta.key:"
				" meta value; see also the documentation.")

		key = key.strip()
		if key.startswith("!"):
			key = key[1:]
			metaContainer.delMeta(key)

		if key not in keysSeen and clearItems:
			metaContainer.delMeta(key)
		keysSeen.add(key)

		value, format = value.strip(), "plain"
		mat = re.match("(rst|literal|raw):", value)
		if mat:
			value = value[mat.end():].strip()
			format = mat.group(1)

		metaContainer.addMeta(key, value, format=format)
				

class MetaParser(common.Parser):
	"""A structure parser that kicks in when meta information is
	parsed from XML.

	This parser can also handle the notation with an attribute-less
	meta tag and lf-separated colon-pairs as content.
	"""
# These are constructed a lot, so let's keep __init__ as clean as possible.
	def __init__(self, container: "MetaMixin", nextParser: common.Parser):
		self.container, self.nextParser = container, nextParser
		self.attrs: Dict[str, StructParserValue]  = {}
		self.children: List[Tuple[str, _MetaSeed, Dict]] = []
		self.next: Optional[str] = None

	def addMeta(self, key: str, content: _MetaSeed = "", **kwargs: Any
			) -> None:
		# this parse can be a temporary meta parent for children; we
		# record them here and play them back when we have created
		# the meta value itself
		self.children.append((key, content, kwargs))

	def setMeta(self, key:str, content: _MetaSeed = "", **kwargs: Any
			) -> None:
		# see addMeta comment on why we need to do magic here.
		self.children.append(("!"+key, content, kwargs))

	def start_(self, ctx: StructParseContext, name: str, value: StructParserValue
			) -> Optional[common.Parser]:
		if name=="meta":
			return MetaParser(cast("MetaMixin", self), self)
		else:
			self.next = name
			return self

	def value_(self, ctx: StructParseContext, name: str, value: StructParserValue
			) -> common.Parser:
		if self.next is None:
			self.attrs[str(name)] = value
		else:
			self.attrs[str(self.next)] = value
		return self

	def _doAddMeta(self) -> None:
		content = cast(str, self.attrs.pop("content_", ""))
		if not self.attrs: # content only, parse this as a meta stream
			parseMetaStream(cast("MetaMixin", self.container), content)

		else:
			try:
				content = utils.fixIndentation(content, "", 1).rstrip()
			except common.Error as ex:
				raise utils.logOldExc(common.StructureError("Bad text in meta value"
					" (%s)"%ex))
			if not "name" in self.attrs:
				raise common.StructureError("meta elements must have a"
					" name attribute")

			metaKey = cast(str, self.attrs.pop("name"))
			if metaKey.startswith("!"):
				self.container.setMeta(metaKey[1:], content, **self.attrs)
			else:
				self.container.addMeta(metaKey, content, **self.attrs)

			# meta elements can have children; add these, properly fudging
			# their keys.  The ignored type is because mypy doesn't see
			# that content is a string here.
			for key, content, kwargs in self.children: # type: ignore
				if key.startswith("!"):
					fullKey = "%s.%s"%(metaKey, key[1:])
					self.container.setMeta(fullKey, content, **kwargs)
				else:
					fullKey = "%s.%s"%(metaKey, key)
					self.container.addMeta(fullKey, content, **kwargs)

	def end_(self, ctx: StructParseContext, name: str, value: StructParserValue
			) -> Optional[common.Parser]:
		if name=="meta":
			try:
				self._doAddMeta()
			except TypeError as msg:
				raise utils.StructureError("While constructing meta: %s"%msg)
			return self.nextParser

		else:
			self.next = None
			return self


class MetaAttribute(attrdef.AttributeDef):
	"""An attribute magically inserting meta values to Structures mixing
	in MetaMixin.

	We don't want to keep metadata itself in structures for performance
	reasons, so we define a parser of our own in here.
	"""
	typedesc = "Metadata"

	def __init__(self, description: str = "Metadata") -> None:
		attrdef.AttributeDef.__init__(self, "meta_",
			attrdef.Computed, description)
		self.aliases = ["meta"]

	@property
	def default_(self) -> Dict[str, "MetaItem"]:
		return {}

	def feedObject(self, instance: Structure, value: "MetaItem") -> None:
		self.meta_ = value

	def getCopy(self,
			parent: Union["MetaMixin", Structure],
			newParent: Union["MetaMixin", Structure],
			ctx: StructParseContext) -> Dict[str, "MetaItem"]:
		"""creates a deep copy of the current meta dictionary and returns it.

		This is used when a MetaMixin's attribute is set to copyable and a
		meta carrier is copied.  As there's currently no way to make the
		_metaAttr copyable, this isn't called by itself.  If you
		must, you can manually call this (_metaAttr.getCopy), but that'd
		really be an indication the interface needs changes.

		Note that the copying semantics is a bit funky: Copied values
		remain, but on write, sequences are replaced rather than added to.
		"""
		parent, newParent = cast(MetaMixin, parent), cast(MetaMixin, newParent)
		oldDict = parent.meta_
		newMeta = {}
		for key, mi in oldDict.items():
			newMeta[key] = mi.copy()
		return newMeta
	
	def create(self, parent: "MetaMixin", ctx: StructParseContext, name: str
			) -> common.Parser:
		return MetaParser(parent, cast(common.Parser, parent))

	def makeUserDoc(self) -> Optional[str]:
		return ("**meta** -- a piece of meta information, giving at least a name"
			" and some content.  See Metadata_ on what is permitted here.")

	def iterEvents(self, instance: Structure) -> Iterator[common.ParserEvent]:
		def doIter(metaDict: Dict[str, "MetaItem"]
				) -> Iterator[common.ParserEvent]:
			for key, item in metaDict.items():
				for value in item:
					yield common.ParserEvent("start", "meta", None)
					yield common.ParserEvent("value", "name", key)
					if value.getContent():
						yield common.ParserEvent("value", "content_", value.getContent())

					if value.meta_:
						yield from doIter(value.meta_)
					yield common.ParserEvent("end", "meta", None)

		yield from doIter(cast("MetaMixin", instance).meta_)


class MetaMixin(common.StructCallbacks):
	"""A mixin for entities carrying meta information.

	The meta mixin provides the following methods:

		- setMetaParent(m) -- sets the name of the meta container enclosing the
			current one.  m has to have the Meta mixin as well.
		- getMeta(key, propagate=True, raiseOnFail=False, default=None) -- returns
			meta information for key or default.
		- addMeta(key, metaItem, moreAttrs) -- adds a piece of meta information
		  here.  Key may be a compound, metaItem may be a text, in which
		  case it will be turned into a proper MetaValue taking key and
		  moreAttrs into account.
		- setMeta(key, metaItem) -- like addMeta, only previous value(s) are
			overwritten
		- delMeta(key) -- removes a meta value(s) for key.

	When querying meta information, by default all parents are queried as
	well (propagate=True).

	Metadata is not copied when the embedding object is copied.
	That, frankly, has not been a good design decision, and there should
	probably be a way to pass copypable=True to the mixin's attribute
	definition.
	"""
	_metaAttr = MetaAttribute()

	def __init__(self) -> None:
		"""is a constructor for standalone use.  You do *not* want to
		call this when mixing into a Structure.
		"""
		self.meta_: Dict[str, "MetaItem"] = {}

	def __hasMetaParent(self) -> bool:
		try:
			self.__metaParent # assert existence
			return True
		except AttributeError:
			return False

	def isEmpty(self) -> bool:
		return len(self.meta_)==0 and getattr(self, "content", "")==""

	def setMetaParent(self, parent: "MetaMixin") -> None:
		if parent is not None:
			self.__metaParent = parent

	def setParent(self, parent: "MetaMixin") -> None:
		if hasattr(parent, "_getMeta"):
			self.setMetaParent(parent)
		super().setParent(parent)

	def getMetaParent(self) -> Optional["MetaMixin"]:
		if self.__hasMetaParent():
			return self.__metaParent
		else:
			return None

	def _getMeta(self,
			atoms: Sequence[str],
			propagate: bool,
			acceptSequence: bool = False) -> "MetaItem":
		try:
			return self._getFromAtom(atoms[0])._getMeta(atoms[1:],
				acceptSequence=acceptSequence)
		except NoMetaKey:
			pass   # See if parent has the key

		if propagate:
			if self.__hasMetaParent():
				return self.__metaParent._getMeta(atoms, propagate,
					acceptSequence=acceptSequence)
			else:
				return CONFIG_META._getMeta(atoms, propagate=False,
					acceptSequence=acceptSequence)

		raise NoMetaKey("No meta item %s"%".".join(atoms), carrier=self)

	def getMeta(self,
			key: str,
			propagate: bool = True,
			raiseOnFail:bool = False,
			default: Optional[str] = None,
			acceptSequence: bool = False) -> Union["MetaItem", Optional[str]]:
		try:
			try:
				return self._getMeta(
					parseKey(key), propagate, acceptSequence=acceptSequence)
			except NoMetaKey as ex:
				if raiseOnFail:
					ex.key = key
					raise
			except MetaCardError as ex:
				raise utils.logOldExc(
					MetaCardError(ex.origMsg, hint=ex.hint, key=key))
		except MetaError as ex:
			ex.carrier = self
			raise
		return default
	
	def hasMeta(self, atom: str, propagate: bool = False) -> bool:
		"""returns true if this meta carrier has an atom metadatum.

		This will *not* parse atom, so you cannot (yet) pass in hierarchical
		meta names.

		With propagate, it will also ask the meta parents.

		In user code, it is generally preferable to just call getMeta
		with raiseOnFail.  This is mainly a shortcut for internal code.
		"""
		if atom in self.meta_:
			return True
		if propagate:
			parent = self.getMetaParent()
			if parent:
				return parent.hasMeta(atom, True)
		return False

	def _iterMeta(self, atoms: Sequence[str]) -> Iterable["MetaValue"]:
		mine, others = atoms[0], atoms[1:]
		for mv in self.meta_.get(mine, []):
			if others:
				for child in mv._iterMeta(others):
					yield child
			else:
				yield mv
			
	def iterMeta(self, key: str, propagate: bool = False
			) -> Iterable["MetaValue"]:
		"""yields all MetaValues for key.

		This will traverse down all branches necessary to yield, in sequence,
		all MetaValues reachable by key.

		If propagation is enabled, the first meta carrier that has at least
		one item exhausts the iteration.

		(this currently doesn't return an iterator but a sequence; that's an
		implementation detail, though.  You should only assume whatever comes
		back is iterable)
		"""
		val: Iterable[MetaValue] = list(self._iterMeta(parseKey(key)))
		if not val and propagate:
			if self.__hasMetaParent():
				val = self.__metaParent.iterMeta(key, propagate=True)
			else:
				val = CONFIG_META.iterMeta(key, propagate=False)
		return val

	def getAllMetaPairs(self) -> Iterator[Tuple[str, "MetaItem"]]:
		"""iterates over all meta items this container has.

		Each item consists of key, MetaValue.  Multiple MetaValues per
		key may be given.

		This will not iterate up, i.e., in general, getMeta will succeed
		for more keys than what's given here.
		"""
		class Accum(MetaBuilder):
			def __init__(self):
				self.items = []
				self.keys = []
			def startKey(self, key: str) -> None:
				self.keys.append(key)
			def enterValue(self, value: MetaValue) -> None:
				self.items.append((".".join(self.keys), value))
			def endKey(self, key: str) -> None:
				self.keys.pop()

		accum = Accum()
		self.traverse(accum)
		return accum.items

	def buildRepr(self,
			key: str,
			builder: "MetaBuilder",
			propagate: bool = True,
			raiseOnFail: bool = True) -> str:
		value = self.getMeta(key, raiseOnFail=raiseOnFail, propagate=propagate)
		if value:
			builder.startKey(key)
			cast(MetaItem, value).traverse(builder)
			builder.endKey(key)
		return builder.getResult()

	def _hasAtom(self, atom: str) -> bool:
		return atom in self.meta_

	def _getFromAtom(self, atom: str) -> "MetaItem":
		if atom in self.meta_:
			return self.meta_[atom]
		raise NoMetaKey("No meta child %s"%atom, carrier=self)

	def getMetaKeys(self) -> List[str]:
		return list(self.meta_.keys())

	# If you are looking for a keys method here: use getMetaKeys;
	# keys has existed up to 2.11, but it was too common a name to keep.

	def _setForAtom(self, atom: str, metaItem: "MetaItem") -> None:
		self.meta_[atom] = metaItem

	def _addMeta(self, atoms: Sequence[str], metaValue: _MetaSeed) -> None:
		primary = atoms[0]
		if primary in self.meta_:
			self.meta_[primary]._addMeta(atoms[1:], metaValue)
		else:
			self.meta_[primary] = MetaItem.fromAtoms(atoms[1:], metaValue)

	def addMeta(self, key: str, metaValue: _MetaSeed, **moreAttrs: Any
			) -> "MetaMixin":
		"""adds metaItem to self under key.

		moreAttrs can be additional keyword arguments; these are used by
		the XML constructor to define formats or to pass extra items
		to special meta types.

		For convenience, this returns the meta container.
		"""
		try:
			if not doMetaOverride(self, key, metaValue, moreAttrs):
				self._addMeta(parseKey(key), ensureMetaValue(metaValue, moreAttrs))
		except MetaError as ex:
			ex.carrier = self
			raise

		return self

	def _delMeta(self, atoms: Sequence[str]) -> None:
		if atoms[0] not in self.meta_:
			return
		if len(atoms)==1:
			del self.meta_[atoms[0]]
		else:
			child = self.meta_[atoms[0]]
			child._delMeta(atoms[1:])
			if child.isEmpty():
				del self.meta_[atoms[0]]

	def delMeta(self, key: str) -> None:
		"""removes a meta item from this meta container.

		This will not propagate, i.e., getMeta(key) might still
		return something unless you give propagate=False.

		It is not an error do delete an non-existing meta key.
		"""
		self._delMeta(parseKey(key))

	def setMeta(self, key: str, value: _MetaSeed, **moreAttrs: Any):
		"""replaces any previous meta content of key (on this container)
		with value.
		"""
		self.delMeta(key)
		self.addMeta(key, value, **moreAttrs)

	def traverse(self, builder: "MetaBuilder") -> None:
		for key, item in self.meta_.items():
			builder.startKey(key)
			item.traverse(builder)
			builder.endKey(key)
	
	def copyMetaFrom(self, other: "MetaMixin") -> None:
		"""sets a copy of other's meta items on self.
		"""
		for key in other.getMetaKeys():
			orig = cast(MetaItem, other.getMeta(key))
			if orig is not None:
				# (orig None can happen for computed metadata)
				self.meta_[key] = orig.copy()

	def makeOriginal(self, key: str) -> None:
		"""marks the meta item key, if existing, as original.

		This is for when a meta container has copied metadata.  DaCHS'
		default behaviour is that a subsequent addMeta will clear the
		copied content.  Call this method for the key in question to
		enable adding to copied metadata.
		"""
		item = self.getMeta(key)
		if item is not None and hasattr(item, "copied"):
			del item.copied


class ComputedMetaMixin(MetaMixin):
	"""A MetaMixin for classes that want to implement defaults for
	unresolvable meta items.

	If getMeta would return a NoMetaKey, this mixin's getMeta will check
	the presence of a _meta_<key> method (replacing dots with two underscores)
	and, if it exists, returns whatever it returns.  Otherwise, the
	exception will be propagated.

	The _meta_<key> methods should return MetaItems; if something else
	is returned, it is wrapped in a MetaValue.

	On copying such metadata, the copy will retain the value on the original
	if it has one.  This does not work for computed metadata that would be
	inherited.
	"""
	def _getFromAtom(self, atom: str) -> "MetaItem":
		try:
			return MetaMixin._getFromAtom(self, atom)
		except NoMetaKey:
			methName = "_meta_"+atom
			if hasattr(self, methName):
				res = getattr(self, methName)()
				if res is None:
					raise
				return ensureMetaItem(res)

			raise

	def getMetaKeys(self) -> List[str]:
		computedKeys = []
		for name in dir(self):
			if name.startswith("_meta_"):
				computedKeys.append(name[6:])
		return MetaMixin.getMetaKeys(self)+computedKeys

	def _iterMeta(self, atoms: Sequence[str]) -> Iterable["MetaValue"]:
		if len(atoms)>1:
			yield from MetaMixin._iterMeta(self, atoms)
		else:
			res: Union[List, MetaItem] = list(MetaMixin._iterMeta(self, atoms))
			if not res:
				try:
					res = self._getFromAtom(atoms[0])
				except NoMetaKey:
					res = []
			for val in res:
				yield val


class MetaItem:
	"""A collection of homogeneous MetaValues.

	All MetaValues within a MetaItem have the same key.

	A MetaItem contains a list of children MetaValues; it is usually
	constructed with just one MetaValue, though.  Use the alternative
	constructor formSequence if you already have a sequence of
	MetaValues.  Or, better, use the ensureMetaItem utility function.

	The last added MetaValue is the "active" one that will be changed
	on _addMeta calls.
	"""
	def __init__(self, val: _MetaSeed):
		self.children: List["MetaValue"] = []
		self.addChild(val)

	@classmethod
	def fromSequence(cls, seq: Sequence[_MetaSeed]) -> "MetaItem":
		res = cls(seq[0])
		for item in seq[1:]:
			res.addChild(item)
		return res

	@classmethod
	def fromAtoms(cls, atoms: Sequence[str], metaValue: _MetaSeed) -> "MetaItem":
		if len(atoms)==0:  # This will become my child.
			return cls(metaValue)

		elif len(atoms)==1:  # Create a MetaValue with the target as child
			mv = MetaValue()
			mv._setForAtom(atoms[0], cls(ensureMetaValue(metaValue)))
			return cls(mv)

		else:   # Create a MetaValue with an ancestor of the target as child
			mv = MetaValue()
			mv._setForAtom(atoms[0], cls.fromAtoms(atoms[1:],
				ensureMetaValue(metaValue)))
			return cls(mv)

	def __str__(self) -> str:
		try:
			res = self.getContent(targetFormat="text")
			return res
		except MetaError:
			return ", ".join(m.getContent(targetFormat="text")
				for m in self.children)

	def __iter__(self) -> Iterator["MetaValue"]:
		return iter(self.children)

	def __len__(self) -> int:
		return len(self.children)

	def __getitem__(self, index: int) -> "MetaValue":
		return self.children[index]

	def isEmpty(self) -> bool:
		return len(self)==0

	def addContent(self, item: _MetaSeed) -> None:
		cast(MetaItem, self.children[-1]).addContent(item)

	def addChild(self,
			metaValue: _MetaSeed = None,
			key: Optional[str] = None) -> None:
# XXX should we force metaValue to be "compatible" with what's
# already in children?
		if hasattr(self, "copied"):
			self.children = []
			delattr(self, "copied")
		if metaValue is None:
			metaValue = MetaValue(None)
		if isinstance(metaValue, str):
			metaValue = MetaValue(metaValue)

		assert isinstance(metaValue, MetaValue)
		self.children.append(metaValue)

	def _getMeta(self, atoms: Sequence[str], acceptSequence: bool = False
			) -> "MetaItem":
		if atoms:
			if len(self.children)!=1 and not acceptSequence:
				raise MetaCardError("Meta sequence in branch for getMeta")
			else:
				return self.children[0]._getMeta(
					atoms, acceptSequence=acceptSequence)
		return self
	
	def _addMeta(self, atoms: Sequence[str], metaValue: _MetaSeed):
# See above for this mess -- I'm afraid it has to be that complex
# if we want to be able to build compound sequences using text labels.

		# Case 1: Direct child of MetaMixin, sequence addition
		if not atoms:
			self.addChild(metaValue)
		else:
			self.children[-1]._addMeta(atoms, metaValue)
		# the other cases are in MetaValue._addMeta

	def getMeta(self, key: str, *args: Any, **kwargs: Any):
		if len(self.children)==1:
			return self.children[0].getMeta(key, *args, **kwargs)
		else:
			raise MetaCardError("No getMeta for meta value sequences",
				carrier=cast(MetaMixin, self), key=key)

	def getContent(self,
			targetFormat: str = "text",
			macroPackage: Optional[MacroPackage] = None,
			acceptSequence: bool = False) -> str:
		if len(self.children)==1 or acceptSequence:
			return self.children[0].getContent(targetFormat, macroPackage)
		raise MetaCardError("getContent not allowed for sequence meta items"
				" ({}).".format(self.children),
			carrier=cast(MetaMixin, self))

	def _delMeta(self, atoms: Sequence[str]) -> None:
		newChildren = []
		for c in self.children:
			c._delMeta(atoms)
			if not c.isEmpty():
				newChildren.append(c)
		self.children = newChildren

	def traverse(self, builder: "MetaBuilder") -> None:
		"""traverses the meta tree, calling methods on builder for each meta
		item found.

		See MetaBuilder for the methods called.
		"""
		for mv in self.children:
			if mv.content:
				builder.enterValue(mv)
			mv.traverse(builder)

	def copy(self) -> "MetaItem":
		"""returns a deep copy of self.
		"""
		newOb = self.__class__("")
		newOb.children = [mv.copy() for mv in self.children]
		# copied attribute evaluated in makeOriginal
		newOb.copied = True  # type: ignore # (dynamic sentinel)
		return newOb

	def serializeToXMLStan(self) -> str:
		return str(self)


class _NoHyphenWrapper(textwrap.TextWrapper):
# we don't want whitespace after hyphens in plain meta strings (looks
# funny in HTML), so fix wordsep_re
	wordsep_re = re.compile(
        r'(\s+|'                                  # any whitespace
        r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))')   # em-dash


class MetaValue(MetaMixin):
	"""is a piece of meta information about a resource.

	The content is always a string.

	The text content may be in different formats, notably

		- literal

		- rst (restructured text)

		- plain (the default)

		- raw (for embedded HTML, mainly -- only use this if you know
			the item will only be embedded into HTML templates).
	"""
	knownFormats = set(["literal", "rst", "plain", "raw"])
	paragraphPat = re.compile("\n\\s*\n")
	consecutiveWSPat = re.compile(r"\s\s+")
	plainWrapper = _NoHyphenWrapper(break_long_words=False,
		replace_whitespace=True)

	def __init__(self, content="", format="plain"):
		self.initArgs = content, format
		MetaMixin.__init__(self)
		if format not in self.knownFormats:
			raise common.StructureError(
				"Unknown meta format '%s'; allowed are %s."%(
					format, ", ".join(self.knownFormats)))
		self.content = content
		self.format = format
		self._preprocessContent()

	def __str__(self) -> str:
		return self.getContent()

	def __repr__(self) -> str:
		return "<meta {}>".format(utils.makeEllipsis(self.getContent(), 30))

	def _preprocessContent(self) -> None:
		if self.format=="plain" and self.content is not None:
			self.content = "\n\n".join(self.plainWrapper.fill(
					self.consecutiveWSPat.sub(" ", para))
				for para in self.paragraphPat.split(self.content))

	def _getContentAsText(self, content: str) -> str:
		return content

	def _getContentAsHTML(self, content: str, block: bool = False) -> StanElement:
		if block:
			encTag = "p"
		else:
			encTag = "span"
		if self.format=="literal":
			return '<%s class="literalmeta">%s</%s>'%(encTag,
				stanxml.escapePCDATA(content), encTag)
		elif self.format=="plain":
			return "\n".join('<%s class="plainmeta">%s</%s>'%(
				encTag, stanxml.escapePCDATA(p), encTag)
				for p in content.split("\n\n"))
		elif self.format=="rst":
# XXX TODO: figure out a way to have them block=False
			return utils.rstxToHTML(content)
		elif self.format=="raw":
			return content

		return None # type: ignore # notreached

	def getExpandedContent(self, macroPackage: Optional[MacroPackage]) -> str:
		if hasattr(macroPackage, "expand") and "\\" in self.content:
			return cast(MacroPackage, macroPackage).expand(self.content)
		return self.content

	def getContent(self,
			targetFormat: str="text",
			macroPackage: Optional["MacroPackage"] = None) -> Union[StanElement, str]:
		content = self.getExpandedContent(macroPackage)
		if targetFormat=="text":
			return self._getContentAsText(content)
		elif targetFormat=="html":
			return self._getContentAsHTML(content)
		elif targetFormat=="blockhtml":
			return self._getContentAsHTML(content, block=True)
		else:
			raise MetaError("Invalid meta target format: %s"%targetFormat)

	def _getMeta(self,
			atoms: Sequence[str],
			propagate: bool = False,
			acceptSequence: bool = False):
		return self._getFromAtom(atoms[0])._getMeta(atoms[1:],
			acceptSequence=acceptSequence)

	def _addMeta(self, atoms: Sequence[str], metaValue: _MetaSeed):
# Cases continued from MetaItem._addMeta
		# Case 2: Part of a compound, metaValue is to become direct child
		if len(atoms)==1:
			primary = atoms[0]

			# Case 2.1: Requested child exists
			if self._hasAtom(primary):
				self._getFromAtom(primary).addChild(metaValue, primary)

			# Case 2.2: Requested child does not exist
			else:
				self._setForAtom(primary, MetaItem(metaValue))

		# Case 3: metaItem will become an indirect child
		else:
			primary = atoms[0]

			# Case 3.1: metaValue will become a child of an existing child of ours
			if self._hasAtom(primary):
				self._getFromAtom(primary)._addMeta(atoms[1:], metaValue)

			# Case 3.2: metaItem is on a branch that needs yet to be created.
			else:
				self._setForAtom(
					primary,
					MetaItem.fromAtoms(atoms[1:], metaValue))

	def copy(self) -> "MetaValue":
		"""returns a deep copy of self.
		"""
		newOb = self.__class__(*self.initArgs)
		newOb.format, newOb.content = self.format, self.content
		newOb.copyMetaFrom(self)
		return newOb


def printMetaTree(metaContainer: MetaMixin, curKey: str = "") -> None:
	#for debugging
	md = metaContainer.meta_
	for childName in md:
		childKey = curKey+"."+childName
		for child in md[childName]:
			print(childKey, child.getContent("text"))
			printMetaTree(child, childKey)


def ensureMetaValue(val: _MetaSeed, moreAttrs: Any={}) -> MetaValue:
	"""makes a MetaValue out of val and a dict moreAttrs unless val already
	is a MetaValue.
	"""
	if isinstance(val, MetaValue):
		return val
	return MetaValue(val, **moreAttrs)


def ensureMetaItem(
		thing: Union[_MetaSeed, List[_MetaSeed]],
		moreAttrs: Any={}) -> MetaItem:
	"""ensures that thing is a MetaItem.

	If it is not, thing is turned into a sequence of MetaValues, which is
	then packed into a MetaItem.

	Essentially, if thing is not a MetaValue, it is made into one with
	moreAttrs.  If thing is a list, this recipe is used for all its items.
	"""
	if isinstance(thing, MetaItem):
		return thing
	
	if isinstance(thing, list):
		return MetaItem.fromSequence(
			[ensureMetaValue(item, moreAttrs) for item in thing])
	
	return MetaItem(ensureMetaValue(thing, moreAttrs))
	

def getMetaText(
		ob: MetaMixin,
		key: str,
		default: Optional[str] = None,
		**kwargs: Any):
	"""returns the meta item key from ob in text form if present, default
	otherwise.

	You can pass getMeta keyword arguments (except default).

	Additionally, there's acceptSequence; if set to true, this will
	return the first item of a sequence-valued meta item rather than
	raising an error.

	ob will be used as a macro package if it has an expand method; to
	use something else as the macro package, pass a macroPackage keyword
	argument.
	"""
	macroPackage = cast(MacroPackage, ob)
	if "macroPackage" in kwargs:
		macroPackage = kwargs.pop("macroPackage")
	acceptSequence = kwargs.get("acceptSequence", False)

	m = ob.getMeta(key, default=None, **kwargs)
	if m is None:
		return default
	try:
		return cast(MetaItem, m).getContent(
			macroPackage=macroPackage, acceptSequence=acceptSequence)
	except MetaCardError as ex:
		raise MetaCardError(ex.origMsg,
			carrier=ex.carrier, hint=ex.hint, key=key)


class MetaBuilder:
	"""A base class for meta builders.

	Builders are passed to a MetaItem's traverse method or to MetaMixin's
	buildRepr method to build representations of the meta information.

	You can override startKey, endKey, and enterValue.  If you are
	not doing anything fancy, you can get by by just overriding enterValue
	and inspecting curAtoms[-1] (which contains the last meta key).

	You will want to override getResult.
	"""
	def __init__(self) -> None:
		self.curAtoms: List[str] = []

	def startKey(self, key: str) -> None:
		self.curAtoms.append(key)
	
	def endKey(self, key: str) -> None:
		self.curAtoms.pop()
		
	def enterValue(self, value: MetaValue) -> None:
		pass
	
	def getResult(self) -> Any:
		pass


class TextBuilder(MetaBuilder):
	"""A MetaBuilder that recovers a tuple sequence of the meta items
	in text representation.
	"""
	def __init__(self) -> None:
		self.metaItems: List[Tuple[str, str]] = []
		super(TextBuilder, self).__init__()
	
	def enterValue(self, value: MetaValue) -> None:
		self.metaItems.append((".".join(self.curAtoms), value.getContent()))
	
	def getResult(self) -> List[Tuple[str, str]]:
		return self.metaItems


def stanFactory(tag: StanElement, **kwargs: Any
		) -> Callable[[List[Any], Optional[Dict[str, Any]]], StanElement]:
	"""returns a factory for ModelBasedBuilder built from a stan-like "tag".
	"""
	def factory(args: List[Any], localattrs: Optional[Dict[str, Any]] = None
			) -> StanElement:
		if localattrs:
			localattrs.update(kwargs)
			attrs = localattrs
		else:
			attrs = kwargs

		if isinstance(tag, type):
			# this presumably is a stanxml.Element.  Instantiate first
			el = tag()
		elif hasattr(tag, "clone"):
			# this ought to be something t.w.template.Tag-like
			el = tag.clone()
		else:
			# assume it's a factory function
			el = tag()

		return el(**attrs)[args]
	return factory


# Within this abomination of code, the following is particularly nasty.
# It *must* go.

class ModelBasedBuilder(MetaBuilder):
	"""A meta builder that can create stan-like structures from meta
	information

	It is constructed with with a tuple-tree of keys and DOM constructors;
	these must work like stan elements, which is, e.g., also true for our
	registrymodel elements.

	Each node in the tree can be one of:
	
		- a meta key and a callable,
		- this, and a sequence of child nodes
		- this, and a dictionary mapping argument names for the callable
			to meta keys of the node; the arguments extracted in this way
			are passed in a single dictionary localattrs.
	
	The callable can also be None, which causes the corresponding items
	to be inlined into the parent (this is for flattening nested meta
	structures).

	The meta key can also be None, which causes the factory to be called
	exactly once (this is for nesting flat meta structures).
	"""
	# describing the constructors to mypy is too tedious for me now.
	def __init__(self, constructors: List[Any], format: str = "text"):
		self.constructors, self.format = constructors, format

	def _buildNode(self,
			processContent: Callable[[List[Any], MetaValue], StanElement],
			metaItem: MetaItem,
			children: Sequence[StanElement] = (),
			macroPackage: Optional[MacroPackage] = None) -> List[StanElement]:

		if not metaItem:
			return []

		result: List[StanElement] = []
		for child in metaItem.children:
			content: List[StanElement] = []
			c = child.getContent(self.format, macroPackage=macroPackage)
			if c:
				content.append(cast(StanElement, c))
			childContent = self._build(children, child, macroPackage)
			if childContent:
				content.append(childContent)
			if content:
				result.append(processContent(content, child))

		return result

	# TODO: If you can't get rid of this horror, at least properly
	# type-annotate it.
	def _getItemsForConstructor(self,
			metaContainer: MetaMixin,
			macroPackage: Optional[MacroPackage],
			key: str,
			factory: Optional[Callable[..., StanElement]],
			children: Sequence[StanElement] = (),
			attrs: Dict[str, Any] = {}) -> List[StanElement]:
		if factory is not None:
			def processContent(childContent: List[Any], metaValue: MetaValue
					) -> StanElement:
				moreAttrs: Dict[str, Any] = {}
				for argName, metaKey in attrs.items():
					val = metaValue.getMeta(metaKey)
					if val:
						moreAttrs[argName] = cast(MetaItem, val).getContent("text",
							macroPackage=macroPackage)
				return cast(StanElement,
					# factory is non-None here, but mypy doesn't see that
					[factory(childContent, localattrs=moreAttrs)]) # type: ignore

		else:
			def processContent(childContent: List[Any], #noflake: conditional def
					metaValue: MetaValue) -> StanElement:
				return cast(StanElement, childContent)

		if key is None:
			return [factory(self._build(children, metaContainer, macroPackage))]
		else:
			return self._buildNode(processContent,
				cast(MetaItem, metaContainer.getMeta(key, raiseOnFail=False)),
				children,
				macroPackage=macroPackage)

	def _build(self,
			constructors: Any,
			metaContainer: MetaMixin,
			macroPackage: Optional[MacroPackage]) -> StanElement:
		result: List[StanElement] = []
		for item in constructors:
			if isinstance(item, str):
				result.append(cast(StanElement, item))
			else:
				try:
					result.extend(
						self._getItemsForConstructor(
							metaContainer,
							macroPackage,
							*item))
				except utils.Error:
					raise
				except:
					raise utils.logOldExc(utils.ReportableError(
						"Invalid constructor func in %s, meta container active %s"%(
							repr(item), repr(metaContainer))))
		return cast(StanElement, result)

	def build(self,
			metaContainer: MetaMixin,
			macroPackage: Optional[MacroPackage] = None) -> StanElement:
		if macroPackage is None:
			macroPackage = cast(MacroPackage, metaContainer)
		return self._build(self.constructors, metaContainer, macroPackage)


# Global meta, items get added
CONFIG_META = MetaMixin()


def makeFallbackMeta(reload: bool = False) -> None:
	"""fills CONFIG_META with items from $configDir/defaultmeta.txt.

	This is called from the module __init__ once typed meta from base
	is in (which means that any typed meta defined in other places won't
	become magic in fallback; regrettably, we can't hold off loading fallback
	meta much longer).
	"""
	srcPath = config.get("configDir") / "defaultmeta.txt"
	if not srcPath.exists():
		# this will be made a bit later in dachs init
		return
	with open(srcPath, "rb") as f:
		content = f.read().decode("utf-8", "ignore")
	parseMetaStream(CONFIG_META, content, clearItems=reload)
