#----------------------------------------------------------------------
#	pooparse.py			JJS   05/21/97
#
#	This module defines the "CmdDef" class, which defines a comman
#	format and the corresponding function to be invoked.  It also
#	defines a few utility functions for resolving object references,
#	etc.
#
#	Generally, the engine can simply call HandleCommand(caller,cmd).
#----------------------------------------------------------------------

import poo
from types import *
from qsplit import *

import string
import regex
import copy

# A Qualifier is something that takes a string, and figures
# out whether it meets certain criteria.  For example, the
# string may be required to evaluate to a positive number,
# or be an object reference, etc.

# global dictionary of qualifiers
QUALS = {}

# global dict mapping object names to objects
gObjNames = {}

# global variable used to define "this", i.e., the object
# on which the currently considered CmdDef was found
gThis = None

class Qualifier:
	def __init__(self,name):
		global QUALS
		self.name = name
		self.specificity = 1
		QUALS[name] = self
	
	def tagpat(self):
		return "\(.+\)"

	def matches(self,s,caller=None):
		# by default, anything matches (except a null string)
		return (s != "")

	def value(self,s,caller=None):
		# by default, just return the string as-is
		return s

class QualStr(Qualifier):
	def value(self,s,caller=None):
		# strip quotes if found
		if s[0] == '"' or s[0] == "'":
			if s[-1] == s[0]: s = s[:-1]
			s = s[1:]
		return s
QualStr("str")

class QualPropname(QualStr):
	def tagpat(self):
		return "\([a-zA-Z0-9_]+\)"
		
	def value(self,s,caller=None):
		if s[0] == '@': return 'at_' + s[1:]
		return s
QualPropname("propname")

class QualFuncdef(QualStr):
	def tagpat(self):
		return "\([a-zA-Z0-9_]+(.*)\)"
QualFuncdef("funcdef")

class QualInteger(Qualifier):
	def __init__(self,name):
		Qualifier.__init__(self,name)
		self.specificity = self.specificity + 10

	def tagpat(self):
		return "\(-?[0-9]+\)"

	def value(self,s,caller=None):
		return string.atoi(s)
QualInteger("int")		

class QualObj(Qualifier):
	def __init__(self,name):
		Qualifier.__init__(self,name)
		self.specificity = self.specificity + 10
		
	def tagpat(self):
		return "\(#[0-9\.]+\|$[a-zA-Z0-9_\.]+\|[a-zA-Z0-9_]+.*\)"

	def matches(self,s,caller):
		if s[0] == '#': return 1
		if s[0] == '$': return 1
		# next line is redundant, because these are in gObjNames, right?
		if s == 'me' or s == 'self' or s == 'caller' or s == 'here':
			return 1
		if '.' in s: return 1
		if s in gObjNames.keys(): return 1
		pos = string.find(s, "'s")
		if pos > 0:		# have something like "Bob's hat"
			objname = s[:pos]
			if objname in gObjNames.keys(): return 1
		return 0

	def value(self,s,caller):
		if s == 'me' or s == 'self' or s == 'caller':
			return caller
		if s == 'here':
			return caller.location
		if s == 'it':
			return caller.it
		if s[0] == '#' or s[0] == '$':
			ob = poo.getObj(s)
			return poo.getObj(s)
		if '.' in s:
			parts = string.split(s,'.')
			try: obj = gObjNames[parts[0]]
			except: return None
			for p in parts[1:]:
				try: obj = getattr(obj,p)
				except: return None
			return obj
		try: return gObjNames[s]
		except: pass
		pos = string.find(s, "'s")
		if pos > 0:		# have something like "Bob's hat"
			try: return gObjNames[s[:pos]].findComponent(s[pos+3:])
			except:pass
		return None

QualObj("obj")

class QualThis(QualObj):
	def matches(self,s,caller):
		# NOTE: this method assumes that gThis has been set to
		#		the object on which this qualifier was found!
		return QualObj.matches(self,s,caller) and \
			QualObj.value(self,s,caller) == gThis
	
	def value(self,s,caller):
		return gThis
QualThis("this")

class QualVal(Qualifier):
	def __init__(self,name):
		Qualifier.__init__(self,name)
		self.specificity = self.specificity + 5
		
	def matches(self,s,caller):
		return 1		# ANYTHING will work as a <val>

	def value(self,s,caller):

		# try it as a number
		try: return string.atoi(s)
		except: pass

		# try it as an object
		out = QUALS['obj'].value(s,caller)
		if out: return out

		# check for special keywords
		if s == 'None' or s == 'none': return None

		# well, maybe it's a list
		if (s[0]=='(' and s[-1]==')') or (s[0]=='[' and s[-1]==']'):
			s = s[1:-1]
			listparts = qsplit_list(s)
		else:
			listparts = qsplit_list(s)
			if len(listparts) < 2: listparts = None
		if listparts:
			# break into pieces, and get the value of each one
			return map(lambda x,me=self,c=caller:me.value(x,c), listparts )

		# see if it's a quoted string
		if s[0] == '"' or s[0] == "'":
			return QUALS['str'].value(s,caller)
		# none of the above?  pass it as a string (for now!)
		return s

QualVal("val")		


#----------------------------------------------------------------------
# CLASS: CmdDef
#
# CmdDef takes a pattern with literal words and tags of
# the form "<tag>" (angle-brackets included).
# It can then compare this to a string and if it matches,
# return a list of words which fit in for each tag.
#----------------------------------------------------------------------

class CmdDef:

	"""CLASS CmdDef:
	This class implements a pattern-matcher, where the pattern
	is formed of words and tags.  It can compare its pattern
	to a string and if it matches, retur a list of words which
	fit in for each tag.
	"""
	
	def __init__(self,owner=None,pat='',funcdef=''):
		self.__dict__['pat'] = pat
		self.__dict__['owner'] = owner
		if not owner:
			# no owner?  either unpickling, or someone is hacking...
			if poo.gOwner:
				raise "NoWayJose", "use setCmdDef to create a command!"
			return
		# compile the given pattern
		self.compile()
		# store the function definition as well (sans any spaces)
		self.__dict__['funcdef'] = string.replace(funcdef,' ','')

	#------------------------------------------------------------------
	# CmdDef method: compile
	#	build an actual regular expression from the given pattern
	#------------------------------------------------------------------
	def compile(self):
		pat = self.pat
		# first, escape any special-meaning characters
		if '.' in pat:
			pat = string.join(string.split(pat,'.'), '\.')
		if '$' in pat:
			pat = string.join(string.split(pat,'$'), '\$')		
		if '*' in pat:
			pat = string.join(string.split(pat,'*'), '\*')		
		charsdone = 0
		inlen = len(pat)
		rexp = ""
		tags = [None]
		while charsdone < inlen:
			tagstart = string.find(pat, '<', charsdone)
			if tagstart >= 0:
				tagend = string.find(pat, '>', tagstart)
				if tagend < 0:
					raise "FormatError", "Unmatched '<' in: " + pat
				tag = pat[tagstart+1:tagend]
				tags.append( QUALS[tag] )
				rexp = rexp + pat[charsdone:tagstart] \
						+ QUALS[tag].tagpat()
				charsdone = tagend + 1
			else:
				rexp = rexp + pat[charsdone:]
				charsdone = inlen
		rexp = rexp + '$'	# match end-of-line
		self.__dict__['rex'] = regex.compile(rexp)
		self.__dict__['tags'] = tuple(tags)
		
	#------------------------------------------------------------------
	# CmdDef pickling support
	#------------------------------------------------------------------
	def __getstate__(self):
		try: del self.__dict__['tagvals']
		except: pass
		try: del self.__dict__['it']
		except: pass
		temp = copy.copy(self.__dict__)
		del temp['rex']
		del temp['tags']
		try: del temp['boundTo']
		except: pass
		return temp

	#------------------------------------------------------------------
	# CmdDef unpickling support
	#------------------------------------------------------------------
	def __setstate__(self,value):
		for key in value.keys():
			self.__dict__[key] = value[key]
		self.compile()

	#------------------------------------------------------------------
	# CmdDef attribute protection
	#------------------------------------------------------------------
	def __setattr__(self,prop,value):
		gOwner = poo.gOwner
		if gOwner and gOwner != self.owner:
			raise "PermError", "Unable to modify " + str(self)
		self.__dict__[prop] = value
	
	#------------------------------------------------------------------
	# CmdDef conversion to string
	#------------------------------------------------------------------
	def __str__(self): return "CmdDef(" + self.pat + ")"
	
	#------------------------------------------------------------------
	# CmdDef METHOD: matches
	#	See if this CmdDef matches the given command string (considering
	#	the current caller, and what we're bound to).  If it does,
	#	return a number indicating the specificity of the match.
	#	If not, return 0.
	#------------------------------------------------------------------
	def matches(self,s,caller):
		global gThis
		self.__dict__['it'] = None
		if self.rex.match(s) < 0:
			return 0
		# pattern matches... now check each tag
		self.__dict__['tagvals'] = [None]
		specificity = len(self.pat)
		gThis = self.boundTo
		for i in range(1,len(self.tags)):
			s = self.rex.group(i)
			if not self.tags[i].matches(s,caller):
				return 0
			val = self.tags[i].value(s,caller)
			if not self.__dict__['it'] and type(val) == InstanceType:
				self.__dict__['it'] = val
			self.tagvals.append( self.tags[i].value(s,caller) )
			specificity = specificity + self.tags[i].specificity
		return specificity
		
	#------------------------------------------------------------------
	# CmdDef METHOD: group
	#------------------------------------------------------------------
	def group(self,*args):
		return apply(self.rex.group, args)

	#------------------------------------------------------------------
	# CmdDef METHOD: value
	#------------------------------------------------------------------
	def value(self,num):
		return self.tagvals[num]
	
	#------------------------------------------------------------------
	# CmdDef METHOD: untag
	#	given a tag string, e.g. "caller" or "%2", return an object
	#	reference, or raise an exception.
	#------------------------------------------------------------------
	def untag(self,tag,caller):
		if tag[0] == '%':
			return self.tagvals[string.atoi(tag[1:])]
		if tag == 'caller':
			return caller
		if tag == 'here':
			return caller.location
		if tag == 'None':
			return None
		# hmm, none of the above?  Try int and string as well
		if tag[0] == '"' or tag[0] == "'":
			return tag[1:-1]
		try: return string.atoi(tag)
		except:
			raise "TagError", "Can't resolve tag value: " + tag
	
	#------------------------------------------------------------------
	# CmdDef METHOD: callFunc
	#	call the function associated with this CmdDef
	#------------------------------------------------------------------	
	def callFunc(self, caller):
		gThis = self.boundTo
		# Find the object on which the function was found.
		# This is set in GetCmdsToCheck.
		funcobj = self.boundTo
		
		periodpos = -1 #periodpos = string.find(self.funcdef,'.')
		parenpos = string.find(self.funcdef,'(')
		if parenpos < 0: parenpos = len(self.funcdef)
		#print "periodpos:", periodpos, "   parenpos:",parenpos
		#if periodpos > 1 and periodpos < parenpos:
		#	funcobj = self.untag(self.funcdef[:periodpos])

		# find the name of the function
		funcname = self.funcdef[periodpos+1:parenpos]
		
		# make sure it exists and is callable
		func = getattr(funcobj,funcname)
		if not callable(func):
			raise "CmdError", funcobj.name + '.' + funcname + " is not callable."

		# now, parse each parameter
		args = []
		if parenpos < len(self.funcdef)-2:
			for i in string.split(self.funcdef[parenpos+1:-1], ','):
				args.append( self.untag( i, caller ) )
		# if 'x' flag is set, just call it the simple way
		if func.x: return apply(func, tuple(args))
		# if 'x' flag is not set, temporarily set it just for this call
		func.__dict__['x'] = 1
		try:
			out = apply(func, tuple(args))
		finally:
			func.__dict__['x'] = 0
		return out


#----------------------------------------------------------------------
# FUNCTION: GatherObjNames
#
#	This function stuffs gObjNames with all the objects local
#	to the caller.  They are stuffed in an order which should
#	(hopefully) retain the most specific one in case of duplicates.
#----------------------------------------------------------------------
def GatherObjNames(caller):
	global gObjNames

	objlists = [caller.contents(), caller.location.contents()]

	# first, stuff special words
	gObjNames = {
		'me':caller,
		'caller':caller,
		'self':caller,
		'here':caller.location
	}
	
	if caller.it: gObjNames['it'] = caller.it
	
	# then, stuff aliases
	for ol in objlists:
		for obj in ol:
			aliases = obj.aliases
			if aliases:
				for alias in obj.aliases:
					gObjNames[alias] = obj
	
	# then, stuff the actual names
	for ol in objlists:
		for obj in ol:
			gObjNames[obj.name] = obj

#----------------------------------------------------------------------
# FUNCTION: ExtractObjects
#
#	This function returns a list of objects which may be referred
#	to in the given string.  It groks anything that starts with
#	'#' or '$', or that matches names in gObjNames.
#----------------------------------------------------------------------
def ExtractObjects(caller, cmdstr, initialList=None):
	global gObjNames
	if initialList: out = initialList
	else: out = []
	words = qsplit(cmdstr)
	for w in filter(lambda x:x,words):
		if w[0] == '#' or w[0] == '$':
			try: obj = poo.getObj(w)
			except: obj = None
			if type(obj) != InstanceType or \
					(obj.__class__ != poo.Obj and \
					 obj.__class__ != poo.User and \
					 obj.__class__ != poo.Directory):
				obj = None
		elif w in gObjNames.keys(): obj = gObjNames[w]
		# LATER: handle multi-word object names here
		else: obj = None
		if obj and obj not in out: out.append(obj)
	return out


#----------------------------------------------------------------------
# FUNCTION: HandleCommand
#
#	This function takes a normal command (i.e., NOT an immediate-mode
#	line of Python code) and attempts to execute it.  It will search
#	the local objects, etc., to match the object references.
#----------------------------------------------------------------------
def HandleCommand(caller, cmdstr):
	global gCmdDefs, gObjNames
	
	# get local object names
	GatherObjNames(caller)

	# pre-processing...
	if cmdstr[0] == '"' or cmdstr[0] == "'":
		cmdstr = "say " + cmdstr
	elif cmdstr[0] == ':':
		cmdstr = "emote " + cmdstr[1:]
	
	# get list of referenced objects, i.e.,
	# objects on which the commands might be defined
	objs = ExtractObjects( caller, cmdstr, [caller, caller.location] )
	
	# find the best command match -- that is, the one that's most specific
	verb = string.split(cmdstr)[0]
	
	bestcmd = None
	bestspec = 0
	for ob in objs:
		# for each object, we need to get the CmdDefs, and also
		# bind those CmdDefs to this object so we know what
		# object to treat as "self" when we invoke the function!
		defs = ob.getCmdDef(verb)
		if verb == ob.name or verb in ob.aliases:
			defs = defs + ob.getCmdDef('<this>')
		for c in defs:
			# bind the CmdDef to the object where it was found
			c.__dict__['boundTo'] = ob
			# check for a match -- keep only the best!
			matchspec = c.matches(cmdstr,caller)
			if matchspec and matchspec > bestspec:
				bestspec = matchspec
				bestcmd = c
				bestcmdobj = ob

	if bestcmd:
		bestcmd.__dict__['boundTo'] = bestcmdobj
		#bestcmd.matches(cmdstr)	# call this again, to restore arg vals
		if bestcmd.it: caller.it = bestcmd.it
		return bestcmd.callFunc(caller)

	# if command doesn't match anything, suggest proper format
	pats = []
	for ob in objs:
		for c in ob.getCmdDef(verb):
			p = string.join(string.split(c.pat,'<this>'),ob.name)
			if p not in pats: pats.append(p)
	if pats:
		print "Improper command.  Try something like the following:"
		for p in pats: print "   ", p
	else:
		print 'Huh?  (Verb "' + verb + '" not found.)'
	return