"""
Exceptions and helper functions for ADQL processing.
"""

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


from gavo import utils
from functools import reduce

class Error(utils.Error):
	"""A base class for the exceptions from this module.
	"""
# XXX todo: We should wrap pyparsing ParseExceptions as well.
	pass

class NotImplementedError(Error):
	"""is raised for features we don't (yet) support.
	"""

class ColumnNotFound(Error, utils.NotFoundError):
	"""is raised if a column name cannot be resolved.
	"""
	def __init__(self, colName, hint=None):
		utils.NotFoundError.__init__(self, colName, "column", "table metadata",
			hint=hint)

class TableNotFound(Error, utils.NotFoundError):
	"""is raised when a table name cannot be resolved.
	"""
	def __init__(self, tableName, hint=None):
		utils.NotFoundError.__init__(self, tableName, "table", "table metadata",
			hint=hint)

class MorphError(Error):
	"""is raised when the expectations of the to-ADQL morphers are violated.
	"""
	pass

class AmbiguousColumn(Error):
	"""is raised if a column name matches more than one column in a
	compound query.
	"""

class NoChild(Error):
	"""is raised if a node is asked for a non-existing child.
	"""
	def __init__(self, searchedType, toks):
		self.searchedType, self.toks = searchedType, toks
	
	def __str__(self):
		return "No %s child found in %s"%(self.searchedType, self.toks)

class MoreThanOneChild(NoChild):
	"""is raised if a node is asked for a unique child but has more than
	one.
	"""
	def __str__(self):
		return "Multiple %s children found in %s"%(self.searchedType,
			self.toks)

class BadKeywords(Error): # pragma: no cover
	"""is raised when an ADQL node is constructed with bad keywords.

	This is a development help and should not occur in production code.
	"""
	def __str__(self):
		return "Bad keywords: "+utils.safe_str(self.args)

class UfuncError(Error):
	"""is raised if something is wrong with a call to a user defined
	function.
	"""

class GeometryError(Error):
	"""is raised if something is wrong with a geometry.
	"""

class RegionError(GeometryError):
	"""is raised if a region specification is in some way bad.
	"""

class FlattenError(Error):
	"""is raised when something cannot be flattened.
	"""

class IncompatibleTables(Error):
	"""is raised when the operands of a set operation are not deemed
	compatible.
	"""

class Absent(object):
	"""is a sentinel to pass as default to nodes.getChildOfType.
	"""


def getUniqueMatch(matches, colName):
	"""returns the only item of matches if there is exactly one, raises an
	appropriate exception if not.
	"""
	if len(matches)==1:
		return matches[0]
	elif not matches:
		raise ColumnNotFound(colName)
	else:
		# Todo: This kind-of, but not quite, compares whether the references
		# actually end up at the same column
		matches = set(matches)
		if len(matches)!=1:
			raise AmbiguousColumn(colName)
		else:
			return matches.pop()


def computeCommonColumns(tableNode):
	"""returns a set of column names that only occur once in the result
	table.

	For a natural join, that's all column names occurring in all tables,
	for a USING join, that's all names occurring in USING, else it's
	an empty set.

	"""
	joinType = getattr(tableNode, "getJoinType", lambda: "CROSS")()
	if joinType=="NATURAL":
		# NATURAL JOIN, collect common names
		return reduce(lambda a,b: a&b,
			[set(t.fieldInfos.columns) for t in tableNode.joinedTables])
	elif joinType=="USING":
		return set(tableNode.joinSpecification.usingColumns)
	else: # CROSS join, comma, etc.
		return set()


class FieldInfoGetter(object):
	"""An abstract class to retrieve table metadata.

	A subclass of this must be passed into adql.parseAnnotating.
	Implementations must fill out the getInfosFor(tableName) method,
	which must return a sequence of (column name, adql.FieldInfo) pairs
	for the named table.

	plain strings for table names will be normalised (lowercased).
	"""
	def __init__(self):
		self.extraFieldInfos = {}
		self.cache = {}

	def normalizeName(self, tableName):
		if isinstance(tableName, str):
			return tableName.lower()
		elif hasattr(tableName, "getNormalized"):
			# a nodes.TableName, presumably
			return tableName.getNormalized()
		else:
			return tableName

	def __call__(self, tableName):
		normalized = self.normalizeName(tableName)
		if normalized in self.extraFieldInfos:
			return self.extraFieldInfos[normalized]

		if normalized not in self.cache:
			self.cache[normalized] = self.getInfosFor(normalized)
		if self.cache[normalized] is None:
			raise utils.NotFoundError(str(normalized), "table",
				"system and uploaded tables")
		return self.cache[normalized]

	def addExtraFieldInfos(self, tableName, fieldInfos):
		"""adds field infos for tableName.

		fieldInfos must be a sequence of (columnName, adql.FieldInfo) pairs.

		Note that tableName is normalised to lowercase here.
		"""
		self.extraFieldInfos[self.normalizeName(tableName)] = fieldInfos

	def getInfosFor(self, tableName):  # pragma: no cover
		raise NotImplementedError("Abstract FieldInfoGetter used!")
