"""
Helpers for making the definition of grammars of the complexity of our
ADQL grammar a bit easier.

Here, we define the AutoBindingGrammar, which is used in adql.grammar to
build a grammar with parseActions defined in adql.nodes, where the
node classes say which symbols they are responsible for.
"""

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


from gavo.adql import nodes


class SingleBindingDict(dict):
	"""A dictionary that lets you assign each key just once.
	"""
	def __setitem__(self, key, value):
		if key in self:
			raise Exception(f"Trying to rebind key: {key}")
		dict.__setitem__(self, key, value)


class AutoBindingGrammar:
	"""A container for pyparsing symbols that receive their parse actions
	from a sequence of "binders"

	The binders are passed into the constructor.  Most of these will be
	derived from nodes.ADQLNode; but they can also be callables with
	parseActionFor set (see the nodes.symbolAction decorator).  Making this more
	generic would take quite a bit of extra thought.  Anyway, what I'm looking at
	here on whatever is in nodes:

	* type: a name of a symbol that should have nodeClass.fromParseResult
	  as a parse action.
	* bindings: as type, but a list of symbol names; when bindings is
	  present, type is ignored.
	* parseActionFor: that's a list of symbol names the "node" (which more
	  likely is just some function) must be called for as a parse action.
	* collapsible: if set on a node class, the parse action will be
	  nodes.autocollapse (that's for transparent nodes I'm eliding out
	  of the tree unless they're more than just a single token).
	"""
	# instance variables must be preset on the class level here so
	# __setattr__ doesn't tick them into the symbols dict.
	_actionForSym = None
	_symbols = None

	def __init__(self, binders):
		self._actionForSym = SingleBindingDict()
		self._symbols = {}

		for binder in binders:
			if hasattr(binder, "type"):
				for binding in getattr(binder, "bindings", [binder.type]):
					if binding:
						self.bind_(binding, binder)

			if hasattr(binder, "parseActionFor"):
				for sym in binder.parseActionFor:
					self._actionForSym[sym] = binder

	def __setattr__(self, symName, value):
		if hasattr(self, symName):
			object.__setattr__(self, symName, value)
			return

		if symName in self._actionForSym:
			value.setParseAction(self._actionForSym[symName])
		self._symbols[symName] = value
	
	def __getattr__(self, symName):
		if symName not in self._symbols:
			raise AttributeError(f"Symbol {symName} not yet defined.")

		return self._symbols[symName]
	
	def bind_(self, symName, nodeClass):
		"""enters nodeClass as a handler for symName.

		This also deals with autocollapsining single nodes.
		"""
		if getattr(nodeClass, "collapsible", False):
			self._actionForSym[symName] = \
				lambda s, pos, toks: nodes.autocollapse(nodeClass, toks)
		else:
			self._actionForSym[symName] = \
				lambda s, pos, toks: nodeClass.fromParseResult(toks)

	def getSymbols_(self):
		"""returns a dict of all symbols defined.
		"""
		return self._symbols
