files_encoding = "cp1250"
matrix_encoding = "cp1250"

class Committee:
	"""Simple container for single committee"""
	def __init__(self, committee, words):
		self.words = words
		self.committee = committee

	def __str__(self):
		str = "["
		for s in self.committee:
			str += s.encode("utf-8") + "; "
		str += "] := ["
		for w in self.words:
			str += w.encode("utf-8") + "; "
		return str + "]"
#		return "%s := %s" % (self.committee, self.words)

class Committees:
	"""Class acting as container for committee set."""
	def __init__(self, filename):
		"""filename - name of .committee file"""
		self.word2committe = {}
		self.committees = []
		self.__load_committees(filename)

	def __parse_committee(self,lines):
		"""Creates Committee out of lines from .committee file."""
		committees_dirt = (lines[0].split("=")[1].strip()).split(",")
		committee_members = []
		
		for cd in committees_dirt:
			cd = cd.strip()
			if len(cd) > 0:
				committee_members.append(cd)
				words_group = []
				for no in xrange(1, len(lines)-1):
					words_group.append( lines[no].strip())
					
		return Committee( set(committee_members), words_group )

	def __build_index(self):
		"""Creates index word->committee"""
		for c in self.committees:

			for w in c.words:
				if w in self.word2committe:
					self.word2committe[w].append(c)
				else:
					self.word2committe[w] = [c]

					
	def __load_committees(self, filename):
		"""Reads committees from file."""
		import codecs
		file = codecs.open(filename, "r", files_encoding)
		stack = []
		for line in file:
			line = line.strip()
			if line[0] == ")":
				in_committee = False
				stack.append(line)
				c = self.__parse_committee(stack)
				self.committees.append(c)
				stack = []
				continue
			stack.append(line)
		self.__build_index()

class WBST_Element:
	"""Simple container for wbst question"""
	def __init__(self, q, a, detractors):
		"""Initalizing element, params:
		q - question word,
		a - correct answer
		detractors - iterable set of detractors with correct answer
		"""
		self.question = q
		self.answer = a
		self.detractors = detractors

class WBST:
	def __init__(self, wbst_filename, committee_filename):
		"""
		wbst_filename - file with wbst questions
		committee_filename - file with committees
		"""
		self.questions = []
		self.committees = Committees(committee_filename)
		print "Loaded %s words in %s committees" % (len(self.committees.word2committe),
													len(self.committees.committees))
		self.__load_data(wbst_filename)
		self.print_results = True
		print "Loaded %s wbst questions" % len(self.questions)

	def __load_data(self, filename):
		"""
		Reads wbst file
		"""
		import codecs
		file = codecs.open(filename, "r", files_encoding)
		first_line = True
		for line in file:
			if first_line:
				first_line = False
				continue
			line = line.strip()
			tokens = line.split(";")
			q = tokens[0]
			a = tokens[1]
			detractors = tokens[2:]
			self.questions.append( WBST_Element(q,a,detractors) )

	def commitees_guess(self, w1, w2):
		"""
		Returns true if w1 and w2 appears at least onec togeather in some words group.
		"""
		if w1 in self.committees.word2committe and w2 in self.committees.word2committe:
			commitees_w1 = self.committees.word2committe[w1]
			for committee in commitees_w1:
				if w2 in committee.words:
					return True
		return False

	def can_guess(self, question):
		"""
		Checks if all words in question were assigned to some committee.
		"""
		if not question.answer in self.committees.word2committe:
			return False
		for w in question.detractors:
			if not w in self.committees.word2committe:
				return False
		return True

	def make_committee_guess(self, q):
		"""
		Returns list of words (from q.detractors) for which pair <q, distractor_i>
		appears togeather in some group of words.
		"""
		answers = []

		q_term = q.question
		
		for w in q.detractors:
			if self.commitees_guess( q_term, w ):
				answers.append( w )
		return answers

	def solve_combined(self, cmp):
		"""
		Solves wbst test using information provided by CBC and compare function.
		"""
		correct_cbc = []
		wrong_cbc = []
		correct_cmp = []
		wrong_cmp = []
		skipped = []
		
		max = len(self.questions)
		curr = 0
		for q in self.questions:
			curr += 1
			if curr % 100 == 0:
				accuracy = float(len(correct_cmp) + len(correct_cbc) ) / float( 
					len(correct_cbc)+len(wrong_cbc)+len(correct_cmp)+len(wrong_cmp) )
				print "Solving... %s / %s (cbc_good:%s, cbc_wrong: %s,cmp_good: %s, cmp_worng: %s, accuracy: %s)" % (curr, max, len(correct_cbc), 
						len(wrong_cbc), len(correct_cmp), len(wrong_cmp), accuracy)

			answers = []
			only_dist = False
			if not self.can_guess( q ):
				only_dist = True
			if not only_dist:
				answers.extend( self.make_committee_guess( q ) )

			if len(answers) > 1:
				a = q.answer
				if cmp.Compare( a.encode(matrix_encoding), q.question.encode(matrix_encoding) ) < 0:
					skipped.append( q )
					continue
				max_sim = -100.0
				choosen = None
				for w in answers:
					sim = cmp.Compare( q.question.encode(matrix_encoding), w.encode(matrix_encoding) )
					if sim < 0:
						choosen = None
						break
					if sim > max_sim:
						choosen = w
						max_sim = sim
				if choosen == None:
					skipped.append( q )
					continue

				if choosen == a:
					correct_cmp.append( q )
				else:
					wrong_cmp.append( q )
				continue

			if only_dist or len(answers) == 0 : 
				a = q.answer
				if cmp.Compare( a.encode(matrix_encoding), q.question.encode(matrix_encoding) ) < 0:
					skipped.append( q )
					continue

				max_sim = -100.0
				choosen = None
				for w in q.detractors:
					sim = cmp.Compare( q.question.encode(matrix_encoding), w.encode(matrix_encoding) )
					if sim < 0:
						choosen = None
						break
					if sim > max_sim:
						choosen = w
						max_sim = sim

				if choosen == None:
					skipped.append( q )
					continue

				if choosen == a:
					correct_cmp.append( q )
				else:
					wrong_cmp.append( q )
				continue


			answer = answers[0]

			if answer == q.answer:
				correct_cbc.append( q )
			else:
				wrong_cbc.append( q )


		accuracy = float(len(correct_cmp) + len(correct_cbc) ) / float( 
				len(correct_cbc)+len(wrong_cbc)+len(correct_cmp)+len(wrong_cmp) )
		if self.print_results:
			print """
			Correct cbc: %s
			Wrong cbc: %s
			Correct distributional: %s
			Wrong distributional: %s
			Accuracy: %s
			Skipped: %s
			""" % (len(correct_cbc),len(wrong_cbc), len(correct_cmp), 
					len(wrong_cmp), accuracy, len(skipped) )

		return (correct_cbc, wrong_cbc, correct_cmp, wrong_cmp)

	def solve_committees_only(self):
		"""
		Solves wbst test using information provided only by CBC.
		"""
		skipped = 0
		correct = []
		wrong = []
		ties = []
		possible_correct = 0
		unable_to_decide = 0
		
		for q in self.questions:
			if not self.can_guess( q ):
				skipped += 1
				continue
			answers = self.make_committee_guess( q )

			if len(answers) == 0:
				unable_to_decide += 1
				continue

			if len(answers) != 1:
				ties.append( q )
				if q.answer in answers:
					possible_correct += 1
				continue

			answer = answers[0]

			if answer == q.answer:
				correct.append( q )
			else:
				wrong.append( q )

		if self.print_results:
			print """
			Correct: %s
			Wrong: %s
			Skipped: %s
			Ties: %s
			Possible correct from ties: %s
			Unable to decide: %s
			""" % (len(correct),len(wrong), skipped, len(ties),
				   possible_correct, unable_to_decide)

		return (correct, wrong, ties)

	def solve_distributional(self, cmp):
		"""
		Check wbst questions form set using distributional compare function cmp.
		"""
		correct = []
		incorrect = []
		skipped = 0

		max = len(self.questions)
		curr = 0
		for q in self.questions:
			curr += 1
			if curr % 100 == 0:
				accuracy = float(len(correct) ) / float( 
					len(correct)+len(incorrect) ) 
				print "Solving... %s / %s (good: %s, wrong:%s,skipped: %s %s)"  % (curr, max, len(correct), 
						len(incorrect), skipped, accuracy)

			a = q.answer
			if cmp.Compare( a.encode(matrix_encoding), q.question.encode(matrix_encoding) ) < 0:
				skipped += 1
				continue

			max_sim = -100.0
			choosen = None

			for w in q.detractors:
				sim = cmp.Compare( q.question.encode(matrix_encoding), w.encode(matrix_encoding) )
				if sim < 0:
					choosen = None
					break
				if sim > max_sim:
					choosen = w
					max_sim = sim

			if choosen == None:
				skipped += 1
				continue

			if choosen == a:
				correct.append( q )
			else:
				incorrect.append( q )

		if self.print_results:
			print """
			Correct: %s
			Incorrect: %s
			Skipped: %s
			""" % ( len(correct), len(incorrect), skipped )

		return (correct, incorrect)
	def check_distributional(self, set, cmp):
		"""
		Check wbst questions form set using distributional compare function cmp.
		"""
		correct = []
		incorrect = []
		skipped = 0

		max = len( set )
		curr = 0
		for q in set:
			curr += 1
			if curr % 100 == 0:
				accuracy = float(len(correct) ) / float( 
					len(correct)+len(incorrect) ) 
				print "Solving... %s / %s (good: %s, wrong:%s,skipped: %s %s)"  % (curr, max, len(correct), 
						len(incorrect), skipped, accuracy)

			a = q.answer
			if cmp.Compare( a.encode(matrix_encoding), q.question.encode(matrix_encoding) ) < 0:
				skipped += 1
				continue

			max_sim = -100.0
			choosen = None

			for w in q.detractors:
				sim = cmp.Compare( q.question.encode(matrix_encoding), w.encode(matrix_encoding) )
				if sim < 0:
					choosen = None
					break
				if sim > max_sim:
					choosen = w
					max_sim = sim

			if choosen == None:
				skipped += 1
				continue

			if choosen == a:
				correct.append( q )
			else:
				incorrect.append( q )

		if self.print_results:
			print """
			Correct: %s
			Incorrect: %s
			Skipped: %s
			""" % ( len(correct), len(incorrect), skipped )

		return (correct, incorrect)

class WNEvaluator:
	"""
	Evaulation of cbc output by comparing it with wordnet senses
	"""
	def __init__(self, db, wngraph, committees, theta, topk):
		self.db = db
		self.wngraph = wngraph
		self.theta = theta
		self.topk = topk
		self.committees = committees
		self.maximum_here = []

	def simW(self, s, lemma):
		"""
		Similarity between a synset s and word lemma
		"""
		max = -1
		#get all units for given lemma
		units = self.db.LexicalUnits.get_units_by_lemma(lemma)
		#get all synsets for those units
		synsets_id = []
		for u in units:
			synsets_id.extend( self.db.Synsets.get_synsets_id_for_unit(u))
		#for each sense of u S_u
		for s_u in synsets_id:
			#check similarity between s and sen_u
			if s == s_u:
				continue

			if not s in self.wngraph.graph.nodes() or not s_u in self.wngraph.graph.nodes():
				continue

			sim = self.wngraph.get_leackock_sim(s, s_u)
			if sim > max:
				max = sim
				if sim > self.theta:
					import networkx
					path = networkx.path.shortest_path(self.wngraph.graph,s, s_u)
					if len(path) > len(self.maximum_here):
						self.maximum_here = path

		return max

	def simC(self, s, c):
		"""
		Similarity between a synset s and committee c
		"""
		k = 0 
		sum = 0.0
		for u in c.words:
			if k >= self.topk: break
			sim = self.simW(s, u)
			if sim > -1:
				k += 1
				sum += self.simW(s, u)

		if k > 0:
			return sum / float(k)
		else:
			return -1
			
	def is_correct(self, word, committee):

		max_sim = 0
		sense = None
		units = self.db.LexicalUnits.get_units_by_lemma(word)
		synsets = []
		for u in units:
			synsets.extend( self.db.Synsets.get_synsets_id_for_unit(u) )
		for s in synsets:
			sim = self.simC(s, committee)
			if sim > max_sim:
				max_sim = sim
				sense = s

		if max_sim > self.theta:
			return (max_sim,sense)

		return (max_sim, None)

	def evaluate(self, filename = 'results.txt'):

		correct = 0
		incorrect = 0
		file = open(filename, 'w')
	
		max = len(self.committees)
		done = 0
		for committee in self.committees:
			words = committee.words
			k =  0
			for w in words:
				
				units = self.db.LexicalUnits.get_units_by_lemma(w)
				synsets_id = []
				for u in units:
					synsets_id.extend( self.db.Synsets.get_synsets_id_for_unit(u))

				correct_synsets = []
				for s in synsets_id:
					if s in self.wngraph.graph.nodes():
						correct_synsets.append(s)

				if len(correct_synsets) == 0:
					#print "No synsets for word %s" % w
					continue;

				if k >= self.topk: break
				k += 1

				result,sense = self.is_correct( w, committee)

				if result > self.theta:
					correct += 1
					c_str = str(committee)
					napis = "GOOD: %s [%s] -> %s\n" %  (c_str,w.encode('utf-8'), self.db.Synsets.get_synset_by_id(sense) )
					file.write(napis)
				elif result > -1:
					c_str = str(committee)
					napis = "BAD: %s [%s] \n" %  (c_str,w.encode('utf-8') )
					file.write(napis)
					incorrect += 1
				else:
					k -= 1
			done += 1
			if done % 20 == 0:
				if correct+incorrect > 0:
					print "%s / %s [%s|%s -> %4f] " % (done,max,correct, incorrect, float(correct)/float(correct+incorrect))

		acc = float(correct)/float(correct+incorrect)
		print "Accuracy: %s\nNumber of words:%s\n" % ( acc, correct+incorrect )
		results_line = "Number of words: %s\nCorrect: %s\nIncorrect: %s\nAccuracy: %s\n" % ( correct+incorrect, correct, incorrect, acc )
		file.write( results_line )
		params_line = "Theta: %s\nK: %s\nMaxlen(in simW) %s" % ( self.theta, self.topk, len(self.maximum_here))
		
		file.write( params_line )
		file.close()

		return correct, incorrect



def main(args):
	import pymatrices, pycomparator, cbc, pyplwn
	from pyplwn import wngraph
	db = pyplwn.database.Database(user="plwn", db="plwn", passwd="plwn")
	wn = wngraph.WNGraph( db.Relations.get_relation_by_name("hiperonimia"), db)


	committes_file = args[1]
	theta = float(args[2])
	k = int(args[3])
	outfilename = args[4]

	print "Committes file: %s\nTheta: %s\nK: %s\nOutput file: %s" % (committes_file, theta, k, outfilename)

	committees = Committees( committes_file )
	print "Loaded %s committees." % len(committees.committees)
	wnev = WNEvaluator(db, wn, committees.committees, theta, k)
	wnev.evaluate( outfilename )



if __name__ == "__main__":
	import sys
	if len(sys.argv) == 5:
		main(sys.argv)
	else:
		print "Usage:\n\tpython cbc_eval.py committe_file theta k outputfilename"

