## Copyright (C) 2001  Glen Wilder <gwilder@best.com>

## This file is part of PyProlog.
## PyProlog is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public
## License as published by the Free Software Foundation; either
## version 2.1 of the License, or (at your option) any later version.

## This library is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
## Lesser General Public License for more details.

## You should have received a copy of the GNU Lesser General Public
## License along with this library; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import sys, types, string, tempfile
from pyprolog import swipl

"""
The low-level interface, swipl, defines types term_ref and atom, where
an atom is not a term.  To make an atom term requires 'putting' an
atom in a term_ref.  The interface here is much simpler: Atom,
Integer, etc are all subclass of Term.

policy: class constructs take prepared items to wrap.  Since the items
are swipl type, the user should not need to call class constructs
directly.  Instead, a number of factory functions are provided.

# TDB: 
signals
exceptions
catch prolog errors
suppressing prolog console I/O
callback
prolog engine configure and query
leak testing
to and from type conversions
class module string
parsing example
threading
database use
predicate properties
operators
arithmetic
list operations
set operations
findall, etc.
maplist, apply, etc
protocol
debug

"""

class TermError(Exception): pass

_term_type = type(swipl.new_term())

def new_atom(name):
	return Atom(_new_atom(name))

def new_integer(value):
	return Integer(_new_integer(value))

def new_float(value):
	return Float(_new_float(value))

def new_string(value):
	return String(_new_string(value))

def new_variable():
	return Variable(_new_variable())

def new_compound(functor, *args):
	return Compound(_new_compound(functor, *args))

def new_list(*args):
	nl = swipl.new_term()
	nl.put_nil()
	if not args:
		return Atom(nl)
	else:
		a = list(args)
		a.reverse()
		m = _make_prolog_args(a)
		for i in range(m.quantity):
			nl.cons_list(m.item(i), nl)
		return List(nl)

def _new_atom(name):
	term = swipl.new_term()
	term.put_atom(swipl.new_atom(name))
	return term

def _new_integer(value):
	term = swipl.new_term()
	term.put_integer(value)
	return term

def _new_float(value):
	term = swipl.new_term()
	term.put_float(value)
	return term

def _new_string(value):
	term = swipl.new_term()
	term.put_string(value)
	return term

def _new_variable():
	term = swipl.new_term()
	term.put_variable()
	return term

def _new_compound(functor, *args):
	arguments = _make_prolog_args(args)
	ft = swipl.new_functor(swipl.new_atom(functor), len(args))
	new_func = swipl.new_term()
	new_func.cons_functor(ft, arguments)
	return new_func

def _ground(term):
	# with low-level term
	if term.is_variable():
		return 0
	if term.is_compound():
		arity = term.get_name_arity()[1]
		for i in range(arity):
			arg = term.get_arg(i+1)
			if not _ground(arg):
				return 0
		return 1
	else:
		return 1

class Term: # for subclassing only.
	def __repr__(self):
		return repr(self.term)
	
	def ground(self):
		return _ground(self.term)
	
	def var(self): 
		return self.term.is_variable()
	
	def nonvar(self): 
		return not self.term.is_variable()
	
	def float(self):
		return self.term.is_float()
	
	def integer(self):
		return self.term.is_integer()
	
	def number(self):
		return self.term.is_number()
	
	def atom(self):
		return self.term.is_atom()
	
	def string(self): 
		return self.term.is_string()
	
	def compound(self): 
		return self.term.is_compound()
	
	def list(self): 
		return self.term.is_list()
	
	def atomic(self): 
		return self.term.is_atomic()
	
	def __cmp__(self):
		pass
	
	def structurally_equal(self): 
		pass
	
	def unify(self, other):
		return self.term.unify(other.term)
	
	def unify_with_occurs_check(self): 
		pass
		
	def concat(self, other):
		head = self.term
		tail = other.term
		newlist = swipl.new_term()
		newlist.cons_list(head, tail)
		return wrap_prolog_term(newlist)

	def accept(self, visitor):
		# double dispatch
		method_name = 'visit_%s' % self.__class__.__name__
		method = getattr(visitor, method_name)
		apply(method, (self,))
		return visitor.result()

class Atom(Term):
	# cache atoms?
	def __init__(self, term):
		assert term.is_atom()
		self.term = term
		
	def get_value(self):
		return self.term.get_chars()
	
	def __repr__(self):
		return 'Atom::%s' % repr(self.term)

class Integer(Term):
	def __init__(self, term):
		self.term = term
	
	def get_value(self):
		return self.term.get_integer()
	
	def __repr__(self):
		return 'Integer::%s' % repr(self.term)

class Float(Term):
	def __init__(self, term):
		self.term = term
			
	def get_value(self):
		return self.term.get_float()
	
	def __repr__(self):
		return 'Float::%s' % repr(self.term)

class String(Term):
	def __init__(self, term):
		self.term = term
	
	def get_value(self):
		return self.term.get_chars()
	
	def __repr__(self):
		return 'String::%s' % repr(self.term)

class Variable(Term):
	def __init__(self, term):
		self.term = term
	
	def get_value(self):
		return self.term.get_chars()
	
	def __repr__(self):
		return 'Variable::%s' % repr(self.term)
	
class Compound(Term):
	def __init__(self, term):
		self.term = term # shallow wrapping
		
	def name_string(self):
		a = self.term.get_name_arity()[0]
		return a.atom_nchars()[1]
		
	def functor(self):
		a = self.term.get_name_arity()[0]
		term_ref = swipl.new_term()
		term_ref.put_atom(a)
		return Atom(term_ref)
		
	def arity(self):
		return self.term.get_name_arity()[1]
	
	def __call__(self, module=None):
		if module:
			r = self.term.call(module)
		else:
			r = self.term.call()
		return r
	
	def get_arg(self, n):
		r = self.term.get_arg(n)
		return wrap_prolog_term(r)
	
	def __repr__(self):
		return 'Compound::%s' % repr(self.term)

class List(Compound):
	# prolog lists are immutable, so they are really more like python
	# tuples.  The final member of a prolog list can be non-nil:
    # [a b c |d], a construct that has no python analog.
	
	def __init__(self, term):
		self.term = term
	
	def __repr__(self):
		return 'List::%s' % repr(self.term)

def _make_prolog_args(args):
	"""
	Predicates need their arguments packed into consecutive argument
	locations.  The low-level interface does this with
	swipl.make_args.  That routine returns a special PTermArray type
	that, hopefully, the user does not need to known about.  This
	routine enhances that packing routine with automatic conversion of
	python type to prolog types.
	"""
	if len(args) < 1:
		raise TypeError, '_make_prolog_args need at least one argument'
	args0 = []
	var_dict = {} # vars with the same name should be the same var
	for i in args:
		#print 'T>', i, type(i)
		if type(i) == types.InstanceType and isinstance(i, Term):
			args0.append(i.term)
		elif type(i) == types.IntType:
			args0.append(_new_integer(i))
		elif type(i) == types.FloatType:
			args0.append(_new_float(i))
		elif type(i) == types.StringType:
			# automatic variable allocations are not accessible by the
			# user.  If you want access, you should make your own.
			if i[0] == '_' and len(i) == 1:
				# always distinct
				args0.append(_new_variable())
			elif i[0] in string.uppercase+'_':
				try:
					v = var_dict[i]
				except KeyError:
					v = _new_variable()
					var_dict[i] = v
				args0.append(v)
			elif i[0] == '"':
				args0.append(_new_string(i[1:-1]))
			else:
				args0.append(_new_atom(i))
		else:
			raise TermError, 'invalid argument type'
	# copy args to contiguous allocation:
	arguments = swipl.make_args(list(args0))
	return arguments

def term_from_string(s): # factory
	term = swipl.chars_to_term(s)
	return wrap_prolog_term(term)

def wrap_prolog_term(term):
	#print '>>', type(term), term
	if term.is_atom():
		return Atom(term)
	elif term.is_integer():
		return Integer(term)
	elif term.is_float():
		return Float(term)
	elif term.is_string():
		return String(term)
	elif term.is_variable():
		return Variable(term)
	elif term.is_list():
		return List(term)
	elif term.is_compound():
		return Compound(term)
	else:
		raise TypeError, 'unknown term type'

class to_prolog:

	def __init__(self, wrapper=wrap_prolog_term):
		self.wrapper = wrapper
	
	def __call__(self, data):
		r = self.dispatch(data)
		return self.wrapper(r)
	
	def dispatch(self, data):
		if type(data) == types.StringType:
			r = self.atom(data)
		elif type(data) == types.IntType:
			r = self.integer(data)
		elif type(data) == types.FloatType:
			r = self.float(data)
		elif type(data) == types.ListType:
			r = self.list(data)
		elif type(data) == types.TupleType:
			r = self.tuple(data)
		else:
			r = self.unknown(data)
		return r
	
	def atom(self, data):
		t = swipl.new_term()
		t.put_atom(swipl.new_atom(data))
		return t
	
	def integer(self, data):
		t = swipl.new_term()
		t.put_integer(data)
		return t
	
	def float(self, data):
		t = swipl.new_term()
		t.put_float(data)
		return t
	
	def list(self, data):
		tail = swipl.new_term()
		tail.put_nil()
		rdata = data
		rdata.reverse()
		for i in rdata:
			head = self.dispatch(i)
			newlist = swipl.new_term()
			newlist.cons_list(head, tail)
			tail = newlist
		return tail
	
	def tuple(self, data):
		return self.list(list(data))
	
	def unknown(self, data):
		raise TypeError, "can't convert value of %s to a prolog term" % type(data)

class var_to_prolog(to_prolog):
	
	def dispatch(self, data):
		if isinstance(data, Variable):
			r = self.variable(data)
		else:
			r = to_prolog.dispatch(self, data)
		return r
	
	def variable(self, data):
		return data.term

class from_prolog:

	def __call__(self, data):
		r = self.dispatch(data)
		return r
	
	def dispatch(self, term):
		if isinstance(term, Term):
			r = self.Term(term)
		elif term.is_atom():
			r = self.atom(term)
		elif term.is_integer():
			r = self.integer(term)
		elif term.is_float():
			r = self.float(term)
		elif term.is_string():
			r = self.string(term)
		elif term.is_variable():
			r = self.variable(term)
		elif term.is_list():
			r = self.list(term)
		elif term.is_compound():
			r = self.compound(term)
		else:
			r = self.unknown(term)
		return r
		
	def Term(self, term):
		# unwrap
		return self.dispatch(term.term)
		
	def atom(self, term):
		r = term.get_chars()
		if r == '[]':
			return self.nil(term)
		else:
			return r
		
	def nil(self, term):
		return []
		
	def integer(self, term):
		return term.get_integer()
		
	def float(self, term):
		return term.get_float()
		
	def string(self, term):
		return term.get_string()
		
	def variable(self, term):
		raise TypeError, "no python type corresponds to a prolog variable"

	def list(self, term):
		r = []
		m = term
		while 1:
			head, tail = m.get_list()
			r.append(self.dispatch(head))
			if tail.is_atom():
				if tail.get_chars() != '[]':
					raise TypeError, "can't convert dotted list"
				else:
					break
			m = tail
		return r
	
	def compound(self, term):
		raise TypeError, "no python type corresponds to a prolog compound"
	
	def unknown(self, term):
		raise TypeError, "unknown term type"

class var_from_prolog(from_prolog):
	
	def variable(self, term):
		return term.get_chars()

class Term_visitor:
	def __init__(self):
		self._res = None
	def result(self):
		return self._res
	def visit_Atom(self, s):
		raise NotImplementedError
	def visit_Integer(self, s):
		raise NotImplementedError
	def visit_Float(self, s):
		raise NotImplementedError
	def visit_String(self, s):
		raise NotImplementedError
	def visit_Compound(self, s):
		raise NotImplementedError
	def visit_Variable(self, s):
		raise NotImplementedError
	def visit_List(self, s):
		raise NotImplementedError
	

def test3():
	c = new_compound('c', 1, 'X')
	d = new_compound('c', 1, 2)
	print c, d
	print c.unify(d)
	print c, d
	print
	
	c = new_compound('c', 'A', 'B')
	d = new_compound('c', 'B', 2)
	print c, d
	print c.unify(d)
	print c, d
	print

	A = new_variable()
	B = new_variable()
	c = new_compound('c', A, B)
	d = new_compound('c', B, 2)
	print c, d
	print c.unify(d)
	print c, d
	print
	
	C = new_variable()
	i = new_integer(13)
	print C
	print i.unify(C)
	print i, C

	j = new_integer(42)
	print j.unify(C)
	print j, C

	print i.unify(C)
	print i, C

if __name__ == '__main__':
	"skip"
	#converter = to_prolog(lambda x: x)
	converter = var_to_prolog()
	t1 = converter('a')
	print t1
	t2 = converter('a b c')
	print t2
	t3 = converter(42)
	print t3
	t4 = converter(-3.14)
	print t4
	t5 = converter([])
	print t5
	t6 = converter([1])
	print t6
	t7 = converter([0, 'one', [21, 22, 23], 3])
	print t7
	print converter(())
	print converter((1,))
	print converter((0, 'one', (21, 22, 23), 3))
	try:
		print converter({})
		print 'error'
	except TypeError, msg:
		pass
	
	X = new_variable()
	Y = new_variable()
	t8 = converter([1, X, 3, X, Y])
	print t8
	
	A = new_variable()
	t9 = converter([A, A, 3, X, Y])
	print t9
	
	print t8.unify(t9)
	print t8
	print t9
	
	print '-*= ' * 10
	restore = from_prolog()
	v = restore(t1); print type(v), v
	v = restore(t2); print type(v), v
	v = restore(t3); print type(v), v
	v = restore(t4); print type(v), v
	v = restore(t5); print type(v), v
	v = restore(t6); print type(v), v
	v = restore(t7); print type(v), v
	try:
		v = restore(t8); print type(v), v
		print 'error'
	except TypeError, msg:
		pass
	
	var_restore = var_from_prolog()
	v = var_restore(t8); print type(v), v
	
	t = swipl.new_term()
	a = swipl.new_term()
	a.put_atom(swipl.new_atom('a'))
	b = swipl.new_term()
	b.put_atom(swipl.new_atom('b'))
	t.cons_list(a,b)
	print t
	t = wrap_prolog_term(t)
	try:
		print restore(t)
		print 'error'
	except TypeError, msg:
		pass
