"""
A renderer for TAP, both sync and async.
"""

#c Copyright 2008-2020, the GAVO project
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import functools
import os

from twisted.internet import threads
from twisted.web import server

from gavo import base
from gavo import svcs
from gavo import utils
from gavo.formal import nevowc
from gavo.protocols import dali
from gavo.protocols import tap
from gavo.protocols import taprunner
from gavo.protocols import uws
from gavo.svcs import streaming
from gavo.web import asyncrender
from gavo.web import common
from gavo.web import grend
from gavo.web import vosi


@functools.lru_cache(1)
def getTAPVersion():
	return base.getMetaText(base.caches.getRD(tap.RD_ID), "tapversion")


class TAPQueryResource(nevowc.TemplatedPage):
	"""the resource executing sync TAP queries.

	While not really going through UWS, this does create a UWS job and
	tears it down later so we can re-use all the parsing and parameter
	interpretation of the TAP job.
	"""
	def __init__(self, service):
		self.service = service
		nevowc.TemplatedPage.__init__(self)

	def _doRender(self, request):
		jobId = tap.WORKER_SYSTEM.getNewIdFromRequest(
			request, self.service)

		try:
			taprunner.runSyncTAPJob(jobId, svcs.QueryMeta.fromRequest(request))

			job = tap.WORKER_SYSTEM.getJob(jobId)
			if job.phase==uws.COMPLETED:
				# This is TAP, so there's exactly one result
				res = job.getResults()[0]
				name, type = res["resultName"], res["resultType"]
				# hold on to the result fd so its inode is not lost when we delete
				# the job.
				f = open(os.path.join(job.getWD(), name), "rb")
				return (f, type)
			elif job.phase==uws.ERROR:
				exc = job.error
				raise base.Error(exc["msg"], hint=exc["hint"])
			elif job.phase==uws.ABORTED:
				raise uws.UWSError("Job was manually aborted.  For synchronous"
					" jobs, this probably means the operators killed it.",
					jobId)
			else:
				raise uws.UWSError("Internal error.  Invalid UWS phase.", jobId)
		finally:
			tap.WORKER_SYSTEM.destroy(jobId)

	def render(self, request):
		threads.deferToThread(self._doRender, request
			).addCallback(self._formatResult, request
			).addErrback(self._serveError, request)
		return server.NOT_DONE_YET

	def _serveError(self, failure, request):
		if not isinstance(failure.value, base.Error):
			base.ui.notifyFailure(failure)
		return dali.serveDALIError(request, failure.value)

	def _formatResult(self, res, request):
		f, type = res

		def writeTable(outputFile):
			utils.cat(f, outputFile)

		request.setHeader("content-type", str(type))
		# if request has an accumulator, we're testing; this might be sync,
		# so we're shortcutting.
		if hasattr(request, "accumulator"):
			writeTable(request)
			request.finish()
		else:
			return streaming.streamOut(writeTable, request)


def getSyncResource(request, service, name):
	if request.uwsArgs["request"]=="getCapabilities":
		return vosi.VOSICapabilityRenderer(request, service)
	else:
		return TAPQueryResource(service)


def parseTAPParameters(request):
	"""returns a coreArgs instance for the TAP parameters in request.
	"""
	# since this is absolutely not the place to parse the input, but
	# the way we've implemented TAP we need to do it here, we
	# operate our context grammar by hand.
	grammar = base.resolveCrossId("//tap#tap-input-grammar")
	ri = grammar.parse(request.strargs)
	list(ri)
	return ri.getParameters()


# TODO: we should probably define different renderers for sync,
# async.  The renderer shouldn't have to dispatch
# like that.

class TAPRenderer(grend.ServiceBasedPage):
	"""A renderer speaking all of TAP (including sync, async, and VOSI).

	Basically, this just dispatches to the sync and async resources.
	"""
	name = "tap"
	urlUse = "base"

	def render(self, request):
		# The root resource  redirects to an info on TAP
		raise svcs.WebRedirect(self.service.getURL("info", absolute=False))

	def _doTAP10CompatCode(self, parameters):
		"""changes request so our TAP 1.1 code keeps working with 1.0 clients.

		This is called once, at the very start of handling stuff.
		"""
		if parameters["responseformat"] is None:
			parameters["responseformat"] = parameters.pop("format", None)

	def getChild(self, name, request):
		request.uwsArgs = parseTAPParameters(request)

		self._doTAP10CompatCode(request.uwsArgs)

		try:
			if (getTAPVersion()!=
					request.uwsArgs.get("version", getTAPVersion())):
				return dali.DALIErrorResource({
					"msg": "Version mismatch; this service only supports"
						" TAP version %s."%getTAPVersion(),
					"type": "ValueError",
					"hint": ""})

			if name==b'sync':
				return getSyncResource(
					request, 
					self.service, 
					name.decode("ascii"))

			elif name==b'async':
				return asyncrender.getAsyncResource(
					request, tap.WORKER_SYSTEM, "tap", self.service, None)

			elif name==b'availability':
				res = vosi.VOSIAvailabilityRenderer(request, self.service)
			elif name==b'capabilities':
				res = vosi.VOSICapabilityRenderer(request, self.service)
			elif name==b'tables':
				res = vosi.VOSITablesetRenderer(request, self.service)
			elif name==b'examples':
				from gavo.web import examplesrender
				res = examplesrender.Examples(request, self.service)
			elif name==b'authcheck':
				from gavo.web import adminrender
				res = adminrender.AuthcheckRenderer(request, self.service)
			else:
				raise svcs.UnknownURI("Bad TAP child '%s'"%(utils.debytify(name)))
			return res
		except svcs.UnknownURI:
			raise
		except base.Error as ex:
			# see flagError in protocols.uws for the reason for the next if
			if not isinstance(ex, (base.ValidationError, uws.JobNotFound)):
				base.ui.notifyError("TAP error")
			return dali.DALIErrorResource(request, ex)
		raise common.UnknownURI("Bad TAP child '%s'"%utils.debytify(name))
