"""
The coverage RD element.

Most code to obtain coverage information from actual resources is in 
gavo.user.info.
"""

#c Copyright 2008-2022, 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 base
from gavo import stc
from gavo import utils
from gavo.utils import pgsphere

class FloatInterval(base.AtomicAttribute):
	"""An attribute keeping a floating-point attribute.

	The literal of these is as in VOTable: a whitespace-separated pair
	for decimal floating-point literals.
	"""
	typeDesc_ = "A float interval (i.e., a space-separated pair of floats)"

	def parse(self, value):
		try:
			lower, upper = [float(f) for f in value.split()]
			if lower>upper:
				raise base.LiteralParseError(self.name_, value,
					hint="In intervals, the lower value must be smaller or equal"
						" to the upper one.")
			return lower, upper
		except (ValueError, TypeError):
			raise base.LiteralParseError(self.name_, value,
				hint="Intervals are represented as two floating-point literals"
				" separated by whitespace.")
	
	def unparse(self, value):
		return "%.10g %.10g"%value


class FloatOrDateInterval(FloatInterval):
	"""An attribute keeping a temporal interval as MJD.

	On literal input, we also allow ISO-like timestamps.  You cannot
	mix MJD and ISO, though.
	"""
	typeDesc_ = "A float or ISO timestamp interval (values separated by space)"
	def parse(self, value):
		try:
			return FloatInterval.parse(self, value)
		except base.LiteralParseError:
			try:
				lower, upper = [stc.dateTimeToMJD(utils.parseISODT(f)) 
					for f in value.split()]
				if lower>upper:
					raise base.LiteralParseError(self.name_, value,
						hint="In intervals, the lower value must be smaller or equal"
							" to the upper one.")
				return lower, upper
			except (ValueError, TypeError):
				raise base.LiteralParseError(self.name_, value,
					hint="Time intervals are represented as two floating-point or"
					" ISO timestamp literals separated by whitespace.")
	

class Ranges(base.ListOfAtomsAttribute):
	"""An attribute definition for a list of intervals.
	"""
	typeDesc_ = "A sequence of intervals (a space-separated pair of floats"

	def __init__(self, name, intervalAttr, default=[], **kwargs):
		base.ListOfAtomsAttribute.__init__(
			self, name, default, intervalAttr, **kwargs)

	def feedFromArray(self, instance, arr):
		"""defines the intervals from a flat array by constructing
		each interval from consecuive pairs.
		"""
		self.feedObject(instance, 
			list(zip(arr[0::2], arr[1::2])))


class Updater(base.Structure):
	"""Information on where and how to update a piece of coverage information.
	"""
	name_ = "updater"

	# this stuff is interpreted by user.limits.iterCoverageItems
	_sourceTable = base.ReferenceAttribute("sourceTable", 
		default=base.NotGiven,
		description="A table from which to compute coverage by default.",
		copyable=True)
	_spaceTable = base.ReferenceAttribute("spaceTable",
		default=base.NotGiven,
		description="A table from which to compute spatial coverage (overrides"
		" sourceTable).",
		copyable=True)
	_timeTable = base.ReferenceAttribute("timeTable",
		default=base.NotGiven,
		description="A table from which to compute temporal coverage (overrides"
		" sourceTable)",
		copyable=True)
	_spectralTable = base.ReferenceAttribute("spectralTable",
		default=base.NotGiven,
		description="A table from which to compute spectral coverage (overrides"
		" sourceTable)",
		copyable=True)
	_mocOrder = base.IntAttribute("mocOrder",
		default=6,
		description="Maximal HEALpix order to use in coverage MOCs (6 is about"
			" a degree resolution, each additional point doubles resolution).",
		copyable=True)


class Coverage(base.Structure):
	"""The coverage of a resource.

	For now, this is attached to the complete resource rather than the table,
	since this is where it sits in VOResource.  DaCHS *could* be a bit
	more flexible, allowing different coverages per publish element.  It
	is not right now, though.

	Note: Technically, this will introduce or amend the coverage meta
	element.  The information given here will be masked if you define
	a coverage meta on the service or table level.  Just do not do that.
	"""
	name_ = "coverage"
	
	_updater = base.StructAttribute("updater",
		childFactory=Updater,
		description="Rules for automatic computation or updating of"
			" coverage information.",
		default=base.NotGiven)
	_spectral = Ranges("spectral", 
		FloatInterval(
			"spectral", description="A wavelength interval"),
		description="Interval(s) of spectral coverage, in Joules of"
		" BARYCENTER vacuum messenger particle energy.")
	_temporal = Ranges("temporal", 
		FloatOrDateInterval(
			"temporal", description="A time interval"),
		description="Interval(s) of temporal coverage, in MJD"
		" (for TT BARYCENTER).")
	_spatial = base.UnicodeAttribute("spatial", 
		default=base.NotGiven,
		description="A MOC in ASCII representation giving the ICRS coverage"
		" of the resource")
	
	def completeElement(self, ctx):
		super().completeElement(ctx)

		if self.spatial is base.NotGiven:
			inj = ctx.getInjected("spatial_coverage", None)
			if inj:
				self.feed("spatial", inj)

		if not self.spectral:
			inj = ctx.getInjected("spectral_coverage", None)
			if inj:
				self._spectral.feedFromArray(self, inj)

		if not self.temporal:
			inj = ctx.getInjected("temporal_coverage", None)
			if inj:
				self._temporal.feedFromArray(self, inj)

		if self.spatial is base.NotGiven:
			self.parsedMOC = None
		else:
			self.parsedMOC = pgsphere.SMoc.fromASCII(self.spatial)
	
	def onParentComplete(self):
		if self.spatial is not base.NotGiven:
			self.parent.addMeta("coverage.spatial", self.spatial)

		for interval in self.temporal:
			self.parent.addMeta("coverage.temporal", 
				self._temporal.itemAttD.unparse(interval))

		for interval in self.spectral:
			self.parent.addMeta("coverage.spectral", 
				self._spectral.itemAttD.unparse(interval))
