"""
PyPal.py
@author: byteface
"""

class PyPal(object):
	"""
    PyPal is the heart for all pypals :)
    """

    # TODO - tell command to pass messages to other pypals. non conflicting. saves having to quit out of current one
    # TODO - list commands
    # TODO - learn from. quick command to copy commands between pypals may be useful. save moving between dirs and copying


    # memory? - obj with funcitons for loading data etc.

	# dictionary that stores object from _meta.json
	o = None

	# TODO - if current context is gone should be able to go through history
	# MULTIPLE CONTEXT OBJECT MAY NEED TO EXISTS. searching for relevant ones is a requirement
	context=None

	# TODO - should every statement should carry certainty?. for now maybe store number 0-1 on here?
	#certainty=0 
	
	# TODO third person, you, actor???... you can 'be' another person
	#perspective={} 

	# the natural language processing engine. eventually will live on a brain object
	nlp=None # TODO - should be an array

	# natural language generation. used for output
	nlg=None # TODO - as above


	def __init__(self,data):
		"""
		data param is obj with unique name. i.e {'name':'pypal'}
		"""
		import json
		with open("bin/%s/_meta.json" % data['name']) as json_file:
			self.o = json.load(json_file)['object']
		
		# TODO - externalise the class
		self.nlp=NLP( self )

		# TODO - externalise the class
		self.nlg=NLG( self )

		#self.context=Context( [self], [self] ) # talk to self


	def introduce(self):
		"""
		introduce - when a pypal is first created this is what it says
		"""
		self.nlg.say( "Hi my name is %s, Thankyou for creating me!" % self.o['name'] )
		self.listen()


	def welcome(self):
		"""
		welcome - whenever you init a pypal
		"""
		self.nlg.say( "%s I see you have returned!" % self.o['friend'] )
		# TODO - display stats?
		self.listen()


	# TODO - listen should really be an open stream at the moment this is just a friend channel.
	# TODO - create channels for pypal>pyal comms
	# TODO - event should be created
	# TODO - should be having thoughts
	def listen(self):

		# NOTE - listen currently considers it to be friend who is talking
		#self_obj = self.nlp.runWordAsFunction( 'bin/%s/brain/perception/self/' % self.o['name'], self.o['name'] )
		#friend_obj = self.nlp.runWordAsFunction( 'bin/%s/brain/perception/physical/animal/human/' % self.o['name'], self.o['friend'] )
		#friend_obj={}

		try:

			# THIS IS A APPARENTLY nix ONLY SOLUTION FOR AUTO PROCESSING
			# IT WILL TIME OUT IF NO INPUT RESPONSE AND RUN AUTOMATIONS
			# steps towards automation. I looked and using mulitprocessing and thread but non can stop a raw_input
			# for now i'm doing this way just as I'm building some content bots and need it sorting

			# the timeout for automation
			#import signal
			#signal.signal(signal.SIGALRM, self.automate)
			#signal.alarm(10)

			#from threading import Timer
			#t=Timer(10,self.automate)
			#t.start()


			self.nlg.say( "I am listening..." )

			import sys
			from select import select

			# TODO - keys presses should reset the timeout
			timeout = 10000 # TODO - add to a pypal config?? - make timeout longer. for testing type automate. have flag/config for autobots?

			rlist, _, _ = select([sys.stdin], [], [], timeout)
			if rlist:
				s = sys.stdin.readline().strip()
				self.process(s)
				self.listen()
			else:
				self.nlg.say( "No input. Automating..." ) # TODO - just run as bg proccess
				self.automate()
				self.listen()

			return
			# NOTE - DOESNT RUN ANYMORE
			# NOTE - old way. preserving here for now until figure this all out
			#self.nlg.say( "I am listening..." )
			#information = raw_input("> ")
			#self.process( information )

			# TODO - parralell process for automation whilst listening?
		
		except:
			self.nlg.log( "FAIL :::::listen" )





	def automate(self,*args,**kwargs):
		"""
		automate is a super simple task runner
		it executes tasks listed in brain/automation/tasks.json
		"""
		
		self.nlg.log( "automate" )

		try:
			# add and run automation.py
			path = 'bin/%s/brain/automation' % self.o['name']
			import sys
			sys.path.append( path )
			task_runner = __import__( 'automate' )
			task_runner.run(self,path)
		except Exception:
			self.nlg.log( "AUTOMATE FAIL!" )

		# TODO - what when automations are complete?. currently returns to listening
		#self.listen()
		return	



	history=[] # TODO - should the history go on the context obj?
	


	## TODO - this should be a HEAR function and should process chunks
	# TODO - this is something that would be also good to parrallel process and decide which streams of informtion to listen to or ignore
	def process(self,information,caller=None,callee=None):

		self.context=Context( self, information ) # FOR NOW JUST FOR STORING PATHS

		self.history.append(information)

		# update the context object
		#self.context=Context( [caller], [callee] )

		# bust all into words, squash whitespace
		words = information.split(None)

		# if its a one letter answer. some helpers/shortcuts
		if len(words)==1:

			# added a repeat function
			if information == 'r':
				print self.history[len(self.history)-2]
				self.process( self.history[len(self.history)-2] )
				return

			# show command history
			if information == 'h':
				for h in history:
					print h
				return


			# TODO - some more 1 key helpers
			# 'r' - repeat last command
			# 'h' - history
			# 'c' - show all available commands in pypal


			self.nlp.processOneWord( information )
			#self.listen()
			return

		self.nlp.processSentence( information )
		#self.listen()
		return


	# TODO - need to ask meaning of words. to at least put it into memory for considering
	# should also be able to check dictionary / nltk sources. but needs to build a program for the word
	def ask_word_meaning(self,word):

		self.nlp.say( "What is '%s'?" % word )
		answer = raw_input("> ")

		# TODO - NO - should probs be processess response
		self.nlp.addNewWord( word, answer )



	# when the bot is not active could explore data sources
	# using a decorator pattern for behaviours on data
#	def explore(self):

		# TODO - let bot decide/choose which data source to consume

		# TODO - can bot find new data sources? from interaction with other bots

		# TODO - to begin with will attempt to buid knowledge graph data sets
		# from webpages/ relational object maps from text

		# can also explore things in the world
		# theres various ways of determining what to explore in the world


		# TODO - create a discover function?...
		# do this by going into unknown. i.e. inventing urls to read.

		# figure out how to chain commands?

		# how to 'think of something to do'



#	def spawn(self):
#	def merge(self,pypal):



# the first job of the context object is to store caller, callee information
# Who is talking and who are they talking to
# NOTE / TODO - this may evolve with time
class Context(object):
	"""
    Context still to be fully defined. 
    
    Will hold things like conversation history and caller/callee information and is used to aid comprehension

    not just personable but subject context

    i.e. if i say show list, then add to list should add to the one ive shown

    hmmmm caller callee is perspective and incorreclty stubbed here. probably why i removed
    the implementation. unless perspective is an object that also resides in a context object?


    NOW GIVES SOME PATH INFO. can access in a command like

    o.context.COMMAND_PATH


    Also forces app into running 1 command at a time. which is good. as thats how a brain kinda works.
    you could probably still spin threads in commands if requried. but we context we have 1 train of thought which is the running command

    """
	
	# both can be lists of animals
	caller=None
	callee=None

	# useful for commands to know where they are loading from
	# so can dump/store stuff there directly when scraping etc.

	#BASEPATH = './bin/%s/brain/commands/add/grades/files3' % o.o['name']
	#BASEPATH = './bin/%s/brain/commands/add/grades/files3' % o.o['name']
	

	BASEPATH=''


	# TODO - may move these 3 onto the NLP. and make context more of a caretaker pattern extended to surmise and store sessions.
	LAST_COMMAND =''
	COMMAND_PATH =''
	#PARAMS=''
	
	def __init__(self, parent, command, caller=None,callee=None):

		# self.caller=caller
		# self.callee=callee

		self.BASEPATH = './bin/%s/brain/commands' % parent.o['name']
		self.LAST_COMMAND = command

		path = '/'.join( self.LAST_COMMAND.split(' ') )
		file = '_'.join( self.LAST_COMMAND.split(' ') ) + '.py'
		#self.COMMAND_PATH = '%s/%s/%s' % ( self.BASEPATH, path, file )
		self.COMMAND_PATH = '%s/%s' % ( self.BASEPATH, path )

		#self.PARAMS='' # NOTE - gets updated once string is parsed




class NLP(object):
	"""
    NLP are processes for word parsing. Generating functions for words.
    Essentially a custom module loader
    """
	owner=None
	
	# TODO -
	#TIME
	#PLACE

	#BASEPATH = './bin/%s/brain/commands/add/grades/files3' % o.o['name']


	def __init__(self,owner):
		self.owner=owner


	def addNewWord( self, word, answer ):
		# TODO - addNewWord. store what friend thinks/says it is
		return


	def processOneWord( self, word ):
		"""
		parse single word commands.
		basically runs a command from the command folder
		"""

		#TODO - check that the word is clean 
		#TODO - see if we know the word
		#TODO - deal with inuendo
		#TODO - lemmatiser maybe required. or we re-routing manually?

		knows_word=False;

		c_path = 'bin/%s/brain/commands' % self.owner.o['name']

		if self.has_command(c_path+"/"+word+"/"+word+".py"):	
			self.owner.nlg.log( "command detected" )
			knows_word=True
			return self.runWordAsFunction( c_path, word )

		if knows_word == False:
			self.owner.nlg.say( "I don't yet know that word" )



	# now that we know input function we can run it.. 
	# TODO - should probably create new problem though and process that way
	# TODO - all the meta properties need updating
	def runWordAsFunction(self,path,word):

		import sys
		sys.path.append( "%s/%s" % (path,word) )		

		try:

			# TODO - check and update all the meta props
			command_module = __import__( word )
			reload(command_module) # reload class without restarting pypal

			return command_module.run(self.owner)

		except Exception, e:
			self.owner.nlg.say( "Sorry, I can't do that, I tried but it didn't work" )
			self.owner.nlg.log( "CHECK YOUR VIRUTAL ENVIRONMENT IS RUNNING." )
			pass


	# TODO - try to find the finite verb
	# NOTE - AT THE MOMENT ONLY PROCESSING COMMANDS
	def processSentence( self, sentence ):
		# print "processSentence"

		words = sentence.split(None)
		word_count = len(words)
		basepath = 'bin/%s/brain/commands' % self.owner.o['name']
		
		word_path_arr=[]

		# walk up the sentence
		for word in words:

			root = basepath+"/"+'/'.join(word_path_arr)
			has_path = self.has_path( root +"/"+ word )
			
			# if next word is the last word. check for a command and run it without params.
			if (len(word_path_arr)+1)==word_count:
				path = root+"/"+word
				function = '_'.join( word_path_arr ) + "_" + word
				if self.has_command(path+"/"+function+".py"):
					return self.runSentenceAsFunction( path, function )


			# if nowhere to go. but there's a command at current path. run it and pass the rest as param
			if (False==has_path):

				function = '_'.join( word_path_arr )

				if self.has_command(root+"/"+function+".py"):

					# get params by removing where we were up to
					params = sentence.replace( ' '.join( word_path_arr ), '' )

					# REMOVE THE WHITE SPACE FROM START OF PARAMS
					params = params[1:]

					# TODO - note. i see i built up to path to strip param. problem here is param is on the command_path. and doesn't get parsed off until here. during execution.
					# TODO - will have a rethink about how want context to work before changing this. so for now will operate on the context obj here
					# TODO - when doing change, nlp ref should probs get given to context. or context keeps them all in array.
					self.owner.context.COMMAND_PATH = self.owner.context.COMMAND_PATH.replace( params, '' )
					#self.owner.context.PARAMS = params

					# TODO - throw error if no param is passed
					if params == None or params == '':
						print 'ERROR:parameter expected. none recieved'

					# run the function
					return self.runSentenceAsFunction( root, function, params )

				else:
					break

			word_path_arr.append(word)


		# TODO - if no command, attempt gnerating reponse from the self compiled programs.
		# TODO - integrate memory, world states, schemas and emotions
		# A LAD is a KAD : cognitive learning

		return self.owner.nlg.say( "No command found" )



	# params at the moment are 'rest of string'
	# long term might break around finite verb and pass whole string?
	def runSentenceAsFunction(self,path,function,params=None):

		#print "runSentenceAsFunction"
		#print path, function, params

		import sys
		sys.path.append( path )

		try:

			# TODO - check all the meta props
			# TODO - may need to also write to some of the meta
			# TODO - if no meta create a default one

			command_module = __import__( function )
			reload(command_module) # reload class without restarting pypal

			if(params!=None):
				return command_module.run(self.owner,params)
			else:
				return command_module.run(self.owner)

			pass

		except Exception, e:
			self.owner.nlg.log( "runSentenceAsFunction FAIL!! \
				\n happens when : \
				\n failing code in the command. i.e imports used by the command not intalled \
				\n venv not running \
				\n not passing params when required" )
			return False
			#self.owner.listen()
		
		pass


	# run several possibilities. decide which is most relevant?
	# the listener as to suppose an ontological truth in each word as they hear it
	# when that doesn't happen even over sets of words things have to be considered
	# and find more context or information. even lead to questioning
	def suppose():
		pass



	## ---------------------------- NLP LANGUGAGE UTILS -----------------------------------

	# check a lookup table of yes words. program needs to be able to expand that list
	# TODO - if one word use lookup, else use NLTK sentimement tool
	# NOTE - false DOES NOT MEAN it is negative, it could be neutral
	def is_string_positive( s ):
		pass

	# check a lookup table of no words. program needs to be able to expand that list
	# TODO - if one word use lookup, else use NLTK sentimement tool
	# NOTE - false DOES NOT MEAN it is positive, it could be neutral
	def is_string_negative( s ):
		pass

	# check a lookup table of 
	# TODO - building lookup tables on the fly is something we need to do
	# RETURN THE NUMBER OR WORD FALSE
	def is_string_number( s ):
		# TODO - check if NLTK can do this
		pass


	def is_math_operator():
		# TODO - check if NLTK can do this
		pass



	## ---------------------------- NLP FILE UTILS -----------------------------------

	# TODO - may get rid of this lookup and have root words as delegators
	def hasParams( self, path, word ):
		"""
		check if parameters True
		"""
		try:
			#TODO - shoud just check if folder has param folder
			import Program
			program = Program.Program( path, word );
			canHasParams = program.meta.get_property( 'rules', 'parameters' );
			return canHasParams
		
		except:
			print "no meta or param found"
			return False # force false if passing a non command. TODO- BUT. we shouldn't be calling if the case.


	def has_path( self, path_to_directory ):
		import os.path
		return os.path.isdir(path_to_directory)


	def has_command(self, path_to_py_file):
		import os.path
		return os.path.isfile(path_to_py_file)



class NLG(object):
	"""
    NLG - generates sentences in the natural language
    at moment just logs strings to output.
    from now on all output should come through here
    """	
	owner=None

	def __init__(self,owner):
		self.owner=owner

	def say( self, words ):
		"""
		output helps distinguish pypals when in the console
		"""
		print "%s : %s" % ( self.owner.o['name'], words )
		return

	# TODO - setup python logger
	# TODO - pass ref to pypal?
	# TODO - logs should write to a file and be accessible by events. i.e. evt12345 - created variable xxx
	def log( self, words ):
		"""
		log differs to the 'say' method.
		log should be more about debugging.
		say should be user comms
		"""

		return # NOTE <<<<<<<<<<<<<<<<<<<<<< im not running

		# TOOD - if debug is true
		import logging
		logging.warning( "------------------------------------- %s : %s" % ( self.owner.o['name'], words ) )
		return
