"""
"Output drivers" for various formats, for the use of form-like renderers.

TODO: Tar and the output format widget should go somewhere else; the
rest should be done by RESPONSEFORMAT and formats.  Then this module
should die.
"""

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

from twisted.internet import threads
from twisted.web.template import tags as T

from gavo import base
from gavo import formats
from gavo import utils
from gavo.formats import csvtable #noflake: format registration
from gavo.formats import jsontable #noflake: format registration
from gavo.formats import fitstable #noflake: format registration
from gavo.formats import texttable #noflake: format registration
from gavo.formats import geojson #noflake: format registration
from gavo.formal import types as formaltypes
from gavo.formal.util import render_cssid
from gavo.protocols import products
from gavo.svcs import customwidgets
from gavo.svcs import streaming
from gavo.web import producttar

__docformat__ = "restructuredtext en"


class ServiceResult(object):
	"""A base class for objects producing formatted output.

	They are basically wrappers for _formatOutput, which
	will usually be used as sources for streaming.streamOut.

	All methods on these objects are class methods -- they are never
	instantiated.

	Deriving classes can override
	
	- _formatOutput(result, request, queryMeta) -- receives the service result
		and arranges for it to be written.
	- canWrite(colSeq) -- a that returns true when a seq of columns
	  can be serialized by this service result.
	- code -- an identifier used in HTTP query strings to select the format
	- label (optional) -- a string that is used in HTML forms to select
	  the format (defaults to label).
	- compute -- if False, at least the form renderer will not run
	  the service (this is when you just return a container).
	"""

	compute = True
	code = None
	label = None

	@classmethod
	def _formatOutput(cls, res, request, queryMeta):
		return b""

	@classmethod
	def getLabel(cls):
		if cls.label is not None:
			return cls.label
		return cls.code

	@classmethod
	def canWrite(cls, colSeq):
		return True


class VOTableResult(ServiceResult):
	"""A ResultFormatter for VOTables.

	The VOTables come as attachments, i.e., if all goes well the form
	will just stand as it is.
	"""
	code = "VOTable"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		if base.getMetaText(data.getPrimaryTable(), "_queryStatus"
				)=="OVERFLOW":
			fName = "truncated_votable.xml"
		else:
			fName = "votable.xml"
		request.setHeader("content-type", base.votableType)
		request.setHeader('content-disposition',
			'attachment; filename=%s'%fName)
		return streaming.streamVOTable(request, data, queryMeta)


class FITSTableResult(ServiceResult):
	"""returns data as a FITS binary table.
	"""
	code = "FITS"
	label = "FITS table"

	@classmethod
	def getTargetName(cls, data):
		if base.getMetaText(data.getPrimaryTable(), "_queryStatus"
				)=="OVERFLOW":
			return "truncated_data.fits", "application/x-fits"
		else:
			return "data.fits", "application/x-fits"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		return threads.deferToThread(fitstable.makeFITSTableFile, data
			).addCallback(cls._serveFile, data, request)

	@classmethod
	def _serveFile(cls, filePath, data, request):
		name, mime = cls.getTargetName(data)
		request.setHeader("content-type", mime)
		request.setHeader('content-disposition',
			'attachment; filename=%s'%name)

		def writeData(dest):
			try:
				with open(filePath, "rb") as f:
					utils.cat(f, dest)
			finally:
				os.unlink(filePath)

		return streaming.streamOut(writeData, request)


class TSVResponse(ServiceResult):
	code = "TSV"
	label = "Text (with Tabs)"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		request.setHeader("content-type", "text/tab-separated-values")

		def produceData(destFile):
			content = texttable.getAsText(data)
			destFile.write(content)

		return streaming.streamOut(produceData, request, queryMeta)


class TextResponse(ServiceResult):
	code = "txt"
	label = "Text (fixed columns)"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		request.setHeader("content-type", "text/plain")
		
		def produceData(destFile):
			formats.formatData("txt", data,
				request, acquireSamples=False)

		return streaming.streamOut(produceData, request, queryMeta)



class CSVResponse(ServiceResult):
	code = "CSV"
	label = "CSV"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		request.setHeader('content-disposition',
			'attachment; filename=table.csv')
		request.setHeader("content-type", "text/csv;header=present")

		def produceData(destFile):
			formats.formatData("csv", data,
				request, acquireSamples=False)

		return streaming.streamOut(produceData, request, queryMeta)


class JsonResponse(ServiceResult):
	code = "JSON"
	label = "JSON"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		request.setHeader('content-disposition',
			'attachment; filename=table.json')
		request.setHeader("content-type", "application/json")
		
		def produceData(destFile):
			formats.formatData("json", data,
				request, acquireSamples=False)
		
		return streaming.streamOut(produceData, request, queryMeta)


class TarResponse(ServiceResult):
	"""delivers a tar of products requested.
	"""
	code = "tar"

	@classmethod
	def _formatOutput(cls, data, request, queryMeta):
		return producttar.getTarMaker().deliverProductTar(
			data, request, queryMeta)

	@classmethod
	def canWrite(cls, colSeq):
		if products.getProductColumns(colSeq):
			return True
		return False


################# Helpers


_getFormat = utils.buildClassResolver(ServiceResult,
	list(globals().values()), key=lambda obj: obj.code)


def getFormat(formatName):
	try:
		return _getFormat(formatName)
	except KeyError:
		raise base.ValidationError("Unknown format '%s'."%formatName,
			"_OUTPUT")


class OutputFormat(object):
	"""A widget that offers various output options in close cooperation
	with gavo.js and QueryMeta.

	The javascript provides options for customizing output that non-javascript
	users will not see.  Also, formal doesn't see any of these.  See gavo.js
	for details.

	This widget probably only makes sense in the Form renderer and thus
	should probably go there.
	"""
	def __init__(self, typeOb, service, queryMeta):
		self.service = service
		self.typeOb = typeOb
		self._computeAvailableFields(queryMeta)
		self._computeAvailableFormats(queryMeta)

	def _computeAvailableFormats(self, queryMeta):
		"""sets the availableFormats property.

		This is a helper for the constructor, inspecting serviceresults' globals().
		"""
		outputFields = self.service.getCurOutputFields(
			queryMeta,
			raiseOnUnknown=False)
		self.availableFormats = [
			(code, format.getLabel())
				for code, format in _getFormat.registry.items()
				if format.canWrite(outputFields)]
		
	def _computeAvailableFields(self, queryMeta):
		"""computes the fields a Core provides but are not output by
		the service by default.

		This of course only works if the core defines its output table.
		Otherwise, availableFields is an empty list.
		"""
		self.availableFields = []
		core = self.service.core
		if (not core.outputTable
				or self.service.getProperty("noAdditionals", False)):
			return
		coreNames = set(f.name for f in core.outputTable)
		defaultNames = set([f.name
			for f in self.service.getHTMLOutputFields(
				queryMeta,
				ignoreAdditionals=True,
				raiseOnUnknown=False)])
		
		for key in coreNames-defaultNames:
			try:
				self.availableFields.append((core.outputTable.getColumnByName(key),
					key in queryMeta["additionalFields"]))
			except KeyError: # Core returns fields not in its table,
		                   # probably computes them
				pass

	def _makeAdditionalSelector(self):
		"""returns an ul element containing form material for additional output
		columns.
		"""
		checkLiterals = {True: "checked", False: None}
		fields = []
		for column, checked in sorted(
				self.availableFields, key=lambda p:p[0].name):
			fields.append(T.tr[
				T.td[
					T.input(type="checkbox", name="_ADDITEM", value=column.key,
						style="width:auto",
						checked=checkLiterals[checked])],
				T.td(style="vertical-align:middle")[
					" %s -- %s"%(column.name, column.description)]])
		return T.table(id="addSelection")[fields]

	def render(self, request, key, args, errors):
		res = T.div(id=render_cssid("_OUTPUT"), style="position:relative")[
			customwidgets.SelectChoice(formaltypes.String(),
				options=self.availableFormats,
				noneOption=("HTML", "HTML")).render(request, "_FORMAT", args, errors)(
					onchange="output_broadcast(this.value)")]

		if self.availableFields:
			res[
				T.div(title="Additional output column selector",
					id=render_cssid("_ADDITEMS"),
					style="display:none")[
							self._makeAdditionalSelector()]]
		return res
	
	renderImmutable = render  # This is a lost case

	def processInput(self, request, key, args):
		return args.get("_FORMAT", ["HTML"])[0]
