# Copyright (C) 2015-2016 Red Hat, Inc. All rights reserved.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import dbus
import threading
# noinspection PyUnresolvedReferences
from gi.repository import GLib
from .job import Job
from . import cfg
from .utils import log_error, mt_async_call, extract_stack_trace


class RequestEntry(object):
	def __init__(self, tmo, method, arguments, cb, cb_error,
			return_tuple=True, job_state=None):
		self.method = method
		self.arguments = arguments
		self.cb = cb
		self.cb_error = cb_error

		self.timer_id = -1
		self.lock = threading.RLock()
		self.done = False
		self._result = None
		self._job = None
		self._rc = 0
		self._rc_error = None
		self._return_tuple = return_tuple
		self._job_state = job_state

		if tmo < 0:
			# Client is willing to block forever
			pass
		elif tmo == 0:
			self._return_job()
		else:
			# Note: using 990 instead of 1000 for second to ms conversion to
			# account for overhead.  Goal is to return just before the
			# timeout amount has expired.  Better to be a little early than
			# late.
			self.timer_id = GLib.timeout_add(
				tmo * 990, RequestEntry._request_timeout, self)

	@staticmethod
	def _request_timeout(r):
		"""
		Method which gets called when the timer runs out!
		:param r:  RequestEntry which timed out
		:return: Result of timer_expired
		"""
		return r.timer_expired()

	def _return_job(self):
		# Return job is only called when we create a request object or when
		# we pop a timer.  In both cases we are running in the correct context
		# and do not need to schedule the call back in main context.
		self._job = Job(self, self._job_state)
		cfg.om.register_object(self._job, True)
		if self._return_tuple:
			self.cb(('/', self._job.dbus_object_path()))
		else:
			self.cb(self._job.dbus_object_path())

	def run_cmd(self):
		try:
			result = self.method(*self.arguments)
			self.register_result(result)
		except SystemExit as se:
			self.register_error(-1, str(se), se)
			raise se
		except dbus.exceptions.DBusException as dbe:
			# This is an expected error path when something goes awry that
			# we handled
			self.register_error(-1, str(dbe), dbe)
		except Exception as e:
			# Use the request entry to return the result as the client may
			# have gotten a job by the time we hit an error
			# Lets set the exception text as the error message and log the
			# exception in the journal for figuring out what went wrong.
			cfg.debug.dump()
			cfg.flightrecorder.dump()
			tb = extract_stack_trace(e)
			log_error("While processing %s: we encountered\n%s" % (str(self.method), tb))
			log_error("Error returned to client: %s" % str(e))
			self.register_error(-1, str(e), e)

	def is_done(self):
		with self.lock:
			rc = self.done
		return rc

	def get_errors(self):
		with self.lock:
			return (self._rc, self._rc_error)

	def result(self):
		with self.lock:
			if self.done:
				return self._result
			return '/'

	def _reg_ending(self, result, error_rc=0, error_msg=None,
					error_exception=None):
		with self.lock:
			self.done = True
			if self.timer_id != -1:
				# Try to prevent the timer from firing
				GLib.source_remove(self.timer_id)

			self._result = result
			self._rc = error_rc
			self._rc_error = error_msg

			if not self._job:
				# We finished and there is no job, so return result or error
				# now!
				# Note: If we don't have a valid cb or cbe, this indicates a
				# request that doesn't need a response as we already returned
				# one before the request was processed.
				if error_rc == 0:
					if self.cb:
						if self._return_tuple:
							mt_async_call(self.cb, (result, '/'))
						else:
							mt_async_call(self.cb, result)
				else:
					if self.cb_error:
						if not error_exception:
							if not error_msg:
								error_exception = Exception(
									"An error occurred, but no reason was "
									"given, see service logs!")
							else:
								error_exception = Exception(error_msg)

						mt_async_call(self.cb_error, error_exception)
			else:
				# We have a job, and it's complete, indicate that it's done.
				self._job.Complete = True
				self._job = None

	def register_error(self, error_rc, error_message, error_exception):
		self._reg_ending('/', error_rc, error_message, error_exception)

	def register_result(self, result):
		self._reg_ending(result)

	def timer_expired(self):
		with self.lock:
			# Set the timer back to -1 as we will get a warning if we try
			# to remove a timer that doesn't exist
			self.timer_id = -1
			if not self.done:
				# Create dbus job object and return path to caller
				self._return_job()
			else:
				# The job is done, we have nothing to do
				pass

		return False
