#!/usr/bin/env python

#   FILE: Pylog.py -- See Pylog class for a description
# AUTHOR: W. Michael Petullo, wp0002@drake.edu
#   DATE: 15 JULY 1997
#
# Copyright (c) 1999 W. Michael Petullo
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice in the documentation and/or other materials provided with
#    the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from string import splitfields, strip
import  PipeDream, os, sys, signal, time
# NOTE: Importing signals may pose a problem for less enlightened operating
#       systems.  See genCGI.py for a possible solution.

DEF_SLEEP = 1     # Default sleep before sending SIGINT to pylogServer when
                  # forcing sanity -- See ForceSanity ()

# ============================ Pylog ===========================================
# NOTE: One should be careful to catch Python run-time errors.  The exception 
#       handling mechanism in a main block of code should call PipeDream's 
#       Shutdown () method upon a run-time error.  This will insure SWI Prolog 
#       shuts down correctly.
#
#       PrologRuntimeError and BadQuery are exceptions that are raised when SWI
#       Prolog encounters a run-time error or bad query, respectively.

PrologRuntimeError = "PylogRuntimeError"
BadQuery           = "BadQuery"

class Pylog:
# ============================ Constructor =====================================
	def __init__ (self, plcmd = "/usr/local/bin/pl", args = ("pl", "-f", "pylogServer.pl", "-t", "pylog:server", "-g", "pylog:server")):
	#  PRE: plcmd is assigned the path to a prolog interpreter
	#   &&  args is assigned a tuple of arguments to pass to plcmd
	# NOTE: The 0ith arg is the name of the program to be executed.
	# POST: this Pylog has been created
		self._pipes = PylogPipes (plcmd, args)
		self._solution = None
		self._pipes.ReceiveProtocol ("ready")
		
# ============================ Query () ========================================
	def Query (self, q):
	#  PRE: q is assigned a string representing a query to send to the 
	#       prolog interpreter
	#   ||  q is assigned ['filename']., where filename is a source file to 
	#       load
	# POST: q has been sent to prolog interpreter
		self._AbortSolution ()
		# ASSERT: any solutions left on pipe from previous query have
		#         been consumed
		if (not q[-1:] == '.'):
			q = q + '.'
			# ASSERT: q has a trailing '.'
		self._pipes.Send (q)
		try:
			self._pipes.ReceiveProtocol ("none")
		except PipeDream.SyncError, message:
			# ASSERT: q was a bad query
			raise BadQuery, message
		self._solution = "query called"
		# ASSERT: a solution is on the pipe (pylogServer automatically
		#         sends the first solution)
		# NOTE: This ensures self._solution will not be left in the
		#       wrong state if a query is made but MoreSolutions () is
		#       never called.  For example, when loading a module.

# ============================ MoreSolutions () ================================
	def MoreSolutions (self, timeout = None):
	#  PRE: timeout is assigned time, in seconds, to wait on pipe for data
	#       a timeout of 0 will cause execution to wait on pipe until data
	#       is available
	# POST: if there is not another solution, FCTVAL == 0
	#       else FCTVAL == 1
		if (timeout == None):
			timeout = self._pipes.DefTimeout ()
		
		line = self._pipes.ReadLine (timeout)
		if (line == "no\n"):
			# ASSERT: no more solutions
			self._solution = None
			self._pipes.ReceiveProtocol ("ready")
			return (0)
		elif (line[0] == ' '):
			# ASSERT: a run-time error occured
			# Read all lines up to and including "ready\n," these
			# lines contain the error message from SWI Prolog.
			message = ""
			while (line != "ready\n"):
				message = message + line
				line = self._pipes.ReadLine (timeout)
			raise PrologRuntimeError, message
			return (0)
		# ASSERT: there is another solution
		self._solution = line
		self._pipes.Send ("next.")
		# ASSERT: at this point there is another solution waiting to be
		#         read off the pipe || there is a "no." waiting to be
		#         read off the pipe
		return (1)

# ============================ GetSolution () ==================================
	def GetSolution (self):
	# POST: FCTVAL == next solution
	# NOTE: self.solution will initialy be in the form
	#       ["X = []","Y = [1, 2, 3]"] when it is received from the
	#       pylogServer.  A dictionary comntaining the state of all the 
	#       variables is what is returned.
		statelist = eval (self._solution)
		statedict = {}
		for x in statelist:
			state = splitfields (x, '=')
			statedict[strip (state[0])] = strip (state[1])
		return (statedict)

# ============================ ForceSanity () ==================================
	def ForceSanity (self):
	# POST: prolog interpreter has been forced into a sane state
		self._AbortSolution ()
		# ASSERT: any solutions left on pipe from previous query have 
		#         been consumed
		# NOTE: A one second pause insures that the SIGINT does not
		#       mess up communication syncronization.  It allows SWI
		#       Prolog to flush any I/O buffer, etc. before getting
		#       interrupted.
		time.sleep (DEF_SLEEP)
		os.kill (self._pipes.ChildPID (), signal.SIGINT)
		
		# Eat "Action (h for help) ? ," which does not contain a CR
		self._pipes.ConsumeUpTo (' ', 5)
		self._pipes.Send ("a")
		self._pipes.ReceiveProtocol ("abort")
		self._pipes.ReceiveProtocol ("ready")

# ============================ Shutdown () =====================================
	def Shutdown (self):
	# POST: prolog interpreter has been shutdown.
		self._pipes.Shutdown ()

# ============================ _AbortSolution () =============================
	def _AbortSolution (self):
	# POST: if a solution remains on the pipe, it will be read off the pipe
	#       and thrown away
	#   &&  current query will be aborted
		if self._solution != None:
			self._pipes.Send ("abort.")
			
			# Eat solution remaining in pipe from previous query.
			line = self._pipes.ReadLine ()
			if (line == "no\n"):
				# ASSERT: there were already no more solutions;
				#         this means a ready is on the pipe
				self._pipes.ReceiveProtocol ("ready")
			self.solution = None
			# ASSERT: no more solutions; there is a ready on the
			#         pipe resulting from the "abort." that was
			#         sent
			self._pipes.ReceiveProtocol ("ready")

# ============================ PylogPipes ======================================
# NOTE: This serves to overload PipeDream's Shutdown method in order to make it
#       properly shutdown pylogServer.py

class PylogPipes (PipeDream.PipeDream):
# ============================ Shutdown () =====================================
	def Shutdown (self):
	# POST: pipes have been closed
	#   &&  child process has been shutdown
		self.Send ("abort.")
		self.Send ("halt.")
		self._pipeIn.close ()
		self._pipeOut.close ()
		self._pipeErr.close ()
		
# ============================ Test Driver =====================================
if (__name__ == "__main__"):
	# First create an instance of a Pylog.
	parser = Pylog ()
	
	try:
	# Use exception handling to insure a proper shutdown of SWI Prolog
	# interpreter.
	
# ==============================================================================
		parser.Query ("append(X,Y,[1,2,3]).")
		parser.MoreSolutions ()
		
		# Now you can issue a Query.
		print ("Making a query...")
		parser.Query ("append(X,Y,[1,2,3]).")
		# As long as there is another solution...
		while (parser.MoreSolutions ()):
			# ...get the next solution.
			solution = parser.GetSolution ()
			print "X = " + solution['X'] + ", Y = " + solution['Y'] 
			# Note that you don't have to retreive all the 
			# solutions if you don't want to..
			
# ==============================================================================
		# If you need to force the SWI Prolog interpreter into a sane 
		# state, the method ForceSanity can be used.
		sys.stdout.write ("Forcing sane state..."); sys.stdout.flush ()
		parser.ForceSanity ()
		print "Okay."
			
# ==============================================================================
		# The Pylog object will catch run-time errors...
		print ("Causing run time error...")
		try:
			parser.Query ("X is 3 / 0.")
			while (parser.MoreSolutions ()):
				sys.stdout.write (parser.GetSolution ())
		except PrologRuntimeError, message:
			sys.stdout.write (message)
			
# ==============================================================================
		# ...and badly formatted queries.	
		print ("Issuing badly formatted query...")
		try:
			parser.Query ("bad(query")
			if (parser.MoreSolutions ()):
				sys.stdout.write (parser.GetSolution ())
		except BadQuery, message:
			sys.stdout.write (message)
					
# ==============================================================================
		# ...Forcing another sane state, just for fun...
		sys.stdout.write ("Forcing sane state..."); sys.stdout.flush ()
		parser.ForceSanity ()
		print "Okay."
		
# ==============================================================================
		# Finally, shutdown the SWI Prolog interpreter.
		print ("Shutting down SWI Prolog interpreter...")
		parser.Shutdown ()
	
# ==============================================================================	
	# If, for some reason, the parent and child process get out of sync in
	# their communication, a pipes.SyncError will be raised.
	
	except PipeDream.SyncError, message:
		print (pipes.SyncError)
		print "The last thing received from the pipe was:"
		print message
		print ("Shutting down SWI Prolog interpreter...")
		parser.Shutdown ()
	except:
		print "unhandled exception encountered"
		parser.Shutdown ()
