"""
Some XML hacks.

StartEndHandler simplifies the creation of SAX parsers, intended for
client code or non-DC XML parsing.

iterparse is an elementtree-inspired thin expat layer; both VOTable
and base.structure parsing builds on it.
"""

#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 collections
import weakref
import xml.sax
from xml.parsers import expat
from xml.sax.handler import ContentHandler

from gavo.utils import excs
from gavo.utils import misctricks
from gavo.utils import texttricks

from gavo.utils.dachstypes import (
	Any, Dict,
	IO, Iterator,
	Optional, PlainXMLEvent,
	StrOrBytes, Tuple, Type)

class ErrorPosition(object):
	"""A wrapper for an error position.

	Construct it with file name, line number, and column.  Use None
	for missing or unknown values.
	"""
	fName = None
	def __init__(self, fName: str, line: int, column: int) -> None:
		self.line = line or '?'
		self.col = column
		if self.col is None:
			self.col = '?'
		self.fName = fName
	
	def __str__(self) -> str:
		if self.fName:
			return "%s, (%s, %s)"%(self.fName, self.line, self.col)
		else:
			return "(%s, %s)"%(self.line, self.col)


class iterparse:
	"""iterates over start, data, and end events in source.

	To keep things simple downstream, we swallow all namespace prefixes,
	if present.

	The events this yields are one of:

	* ("start", element-name, attributes)
	* ("end", element-name, None)
	* ("data", None, payload)
	
	utils.dachstypes.PlainXMLEvent is a type for these.

	iterparse is constructed with a source (anything that can read(source))
	and optionally a custom error class.  This error class needs to
	have the message as the first argument.  Since expat error messages
	usually contain line number and column in them, no extra pos attribute
	is supported.

	Since the parser typically is far ahead of the events seen, we
	do our own bookkeeping by storing the parser position with each
	event.  The *end* of the construct that caused an event can
	be retrieved using pos.
	"""
	chunkSize: int = 2**20
	"The number of bytes handed to expat from iterparse at one go."

	def __init__(self,
			source: IO,
			parseErrorClass: Type[excs.Error] = excs.StructureError) -> None:
		self.source = source
		self.parseErrorClass = parseErrorClass

		if hasattr(source, "name"):
			self.inputName = source.name
		elif hasattr(source, "getvalue"):
			self.inputName = texttricks.makeEllipsis("IO:'"
					+texttricks.safe_str(source.getvalue()))+"'"
		else:
			self.inputName = texttricks.makeSourceEllipsis(source)

		self.parser = expat.ParserCreate()
		self.parser.buffer_text = True
		self.lastLine, self.lastColumn = 1, 0
		self.evBuf: collections.deque[Tuple[
				PlainXMLEvent, Optional[Tuple[int, int]]]
			] = collections.deque()
		self.parser.StartElementHandler = self._startElement
		self.parser.EndElementHandler = self._endElement
		self.parser.CharacterDataHandler = self._characters

	def __iter__(self) -> Iterator[PlainXMLEvent]:
		return self
	
	def _startElement(self, name: str, attrs: Dict[str, str]) -> None:
		self.evBuf.append(
			(("start", name.split(":")[-1], attrs),
				(self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber)))
	
	def _endElement(self, name: str) -> None:
		self.evBuf.append((("end", name.split(":")[-1], None),
			(self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber)))
	
	def _characters(self, data: str) -> None:
		self.evBuf.append((("data", None, data), None))

	def pushBack(self, type: str, name: str, payload: str) -> None:
		self.evBuf.appendleft(((type, name, payload), None))

	def __next__(self) -> PlainXMLEvent:
		while not self.evBuf:
			try:
				nextChunk = self.source.read(self.chunkSize)
				if nextChunk:
					self.parser.Parse(nextChunk)
				else:
					self.close()
					break
			except expat.ExpatError as ex:
				srcDesc = getattr(self.source, "name", "(internal source)")
				newEx = self.parseErrorClass(srcDesc+" "+str(ex))
				# see base.xmlstruct for the following ignore.
				newEx.posInMsg = True  # type: ignore
				newEx.inFile = srcDesc # type: ignore
				raise misctricks.logOldExc(newEx)

		if not self.evBuf:
			raise StopIteration("End of Input")
		event, pos = self.evBuf.popleft()
		if pos is not None:
			self.lastLine, self.lastColumn = pos
		return event

	def close(self) -> None:
		self.parser.Parse("", True)
		self.parser.StartElementHandler =\
		self.parser.EndElementHandler = \
		self.parser.CharacterDataHandler = None

	@property
	def pos(self) -> ErrorPosition:
		return ErrorPosition(self.inputName, self.lastLine, self.lastColumn)

	def getParseError(self, msg: str) -> Exception:
		"""returns an instance of what you passed in in parseErrorClass
		with location information.
		"""
		res = self.parseErrorClass("At %s: %s"%(self.pos, msg))
		# see base.xmlstruct for the following ignore
		res.posInMsg = True # type: ignore
		return res


# Not type annotating the SAX interface methods.  See xml.sax.handler
# declarations for these.
class StartEndHandler(ContentHandler):
	"""This class provides startElement, endElement and characters
	methods that translate events into method calls.

	When an opening tag is seen, we look of a _start_<element name>
	method and, if present, call it with the name and the attributes.
	When a closing tag is seen, we try to call _end_<element name> with
	name, attributes and contents.	If the _end_xxx method returns a
	string (or similar), this value will be added to the content of the
	enclosing element.

	Rather than overriding __init__, you probably want to override
	the _initialize() method to create the data structures you want
	to fill from XML.

	StartEndHandlers clean element names from namespace prefixes, and
	they ignore them in every other way.  If you need namespaces, use
	a different interface.
	"""
	def __init__(self):
		ContentHandler.__init__(self)
		self.realHandler = weakref.proxy(self)
		self.elementStack = []
		self.contentsStack = [[]]
		self._initialize()

	def _initialize(self) -> None:
		pass

	def processingInstruction(self, target: str, data:str) -> None:
		self.contentsStack[-1].append(data)

	def cleanupName(self, name: str) -> str:
		"""returning an element name replacing _ for -.

		That's so people can define methods for such names, and if that
		leads to clashes, the schema designers were sadists.
		"""
		return name.split(":")[-1].replace("-", "_")

	def startElementNS(self, namePair, qName, attrs):
		newAttrs = {}
		for ns, name in list(attrs.keys()):
			if ns is None:
				newAttrs[name] = attrs[(ns, name)]
			else:
				newAttrs["{%s}%s"%(ns, name)] = attrs[(ns, name)]
		self.startElement(namePair[1], newAttrs)

	def startElement(self, name: str, attrs) -> None:
		self.contentsStack.append([])
		name = self.cleanupName(name)
		self.elementStack.append((name, attrs))
		if hasattr(self.realHandler, "_start_%s"%name):
			getattr(self.realHandler, "_start_%s"%name)(name, attrs)
		elif hasattr(self, "_defaultStart"):
			self._defaultStart(name, attrs)

	def endElementNS(self, namePair, qName):
		self.endElement(namePair[1])

	def endElement(self, name: str, suppress: Optional[bool]=False) -> None:
		contents = "".join(self.contentsStack.pop())
		name = self.cleanupName(name)
		_, attrs = self.elementStack.pop()
		res = None
		if hasattr(self.realHandler, "_end_%s"%name):
			res = getattr(self.realHandler,
				"_end_%s"%name)(name, attrs, contents)
		elif hasattr(self, "_defaultEnd"):
			res = self._defaultEnd(name, attrs, contents)
		if isinstance(res, str) and not suppress:
			self.contentsStack[-1].append(res)

	def characters(self, chars):
		self.contentsStack[-1].append(chars)
	
	def getResult(self) -> Any:
		return self.contentsStack[0][0]

	def getParentTag(self, depth:int=1) -> Optional[str]:
		"""Returns the name of the parent element.

		This only works as written here in end handlers.  In start handlers,
		you have to path depth=2 (since their tag already is on the stack).
		"""
		if self.elementStack:
			return self.elementStack[-depth][0]
		return None
	
	def parse(self, stream: IO) -> ContentHandler:
		xml.sax.parse(stream, self)
		return self
	
	def parseString(self, string: StrOrBytes) -> "StartEndHandler":
		xml.sax.parseString(string, self)
		return self

	# xml.sax is smart enough to do the right thing when it gets passed bytes.
	# TODO: to away with parseBytes.
	def parseBytes(self, data: StrOrBytes) -> "StartEndHandler":
		return self.parseString(data)

	def getAttrsAsDict(self, attrs):
		"""returns attrs as received from SAX as a dictionary.

		The main selling point is that any namespace prefixes are removed from
		the attribute names.  Any prefixes on attrs remain, though.
		"""
		return dict((k.split(":")[-1], v) for k, v in list(attrs.items()))
	
	def setDocumentLocator(self, locator):
		self.locator = locator


def traverseETree(eTree):
	"""iterates the elements of an elementTree in postorder.
	"""
	for child in eTree:
		for gc in traverseETree(child):
			yield gc
	yield eTree
