"""Query helper classes for searching a PGCatalogInfospace.

The PGCatalogInfospace's search() method expects an expression, which can
be either an instance of Aggregation (more specifically, Or or And), or
an instance of Criterion."""

class WeightedSet:
	"""An weighted set"""
	
	def __init__(self,data=None):
		"""A set can be initialized with a dictionary or with a WeightedSet instance"""
		self.__data = {}
		if data:
			assert type(data) is dict or isinstance(data,WeightedSet)
			if type(data) is dict:
				self.__data = data
			else:
				self.__data = data.get_set_data()
				
	def get_set_data(self):
		return self.__data
		
	def as_sorted_list(self):
		""" Sorts the result set in order of decreasing weight,
		then returns a list of tuples (result,weight)"""
# 		def cmpfunc(a,b): cmp(a[1], b[1])

		tmplist = [(weight,objectkey) for objectkey,weight in self.__data.iteritems()]
		tmplist.sort()
		tmplist.reverse()
		mylist = [ (objectkey,weight) for (weight,objectkey) in tmplist]
		
		return mylist
	
	def intersection_update(self,another_set):
		assert isinstance(another_set,WeightedSet)
		dictionary = another_set.get_set_data()
		keys = dictionary.keys()
		for key in keys:
			if self.__data.has_key(key): # we average the two values
				self.__data[key] = (self.__data[key] + dictionary[key]) / 2
			else: pass # we pass it
# 				del self.__data[key]

	def union_update(self,another_set):
		assert isinstance(another_set,WeightedSet)
		dictionary = another_set.get_set_data()
		keys = dictionary.keys()
		for key in keys:
			if self.__data.has_key(key):
				self.__data[key] = self.__data[key] + dictionary[key]
			else:
				self.__data[key] = dictionary[key]

class ResultSet(WeightedSet):
	"""A weighted search results set.  To use, you can intersect or
	union with other sets, or use the as_sorted_list() method."""
	pass

from types import StringTypes

def __is_expression(expr):
	return (type(expr) is dict)

def __is_query_expression(expr):
	return __is_expression(expr) and expr.has_key("expression")

def __is_aggregation_expression(expr):
	return __is_expression(expr) and expr.has_key("expressions") and expr.has_key("aggregation")

def __is_or(expr):
	return __is_aggregation_expression(expr) and expr["aggregation"] == "or"

def __is_and(expr):
	return __is_aggregation_expression(expr) and expr["aggregation"] == "and"

def __is_criterion_expression(expr):
	return __is_expression(expr) and expr.has_key("attribute") and expr.has_key("value") and expr.has_key("operator")


def normalize_expression(query):
	"""Normalize a passed list/dict based expression, returning a
	new expression."""
	return __normalize_expression(query)

def __normalize_expression(query,level=0):
# 	logging.getLogger("util").debug("    "*(level)+"convert_to_expression called with " + str(query))
	
	if not __is_expression(query):
		raise ValueError, "Invalid expression passed to expression converter"
	
	if __is_aggregation_expression(query):
		exps = []
		for expr in query["expressions"]:
			exps.append(__normalize_expression(expr))
		if __is_or(query): aggregation = Or(exps)
		elif __is_and(query): aggregation = And(exps)
		else: raise ValueError, "Invalid operator expression"
		return aggregation
		
	elif __is_criterion_expression(query):
		crit = Criterion()
		crit.attribute = query["attribute"]
		if not type(crit.attribute) in StringTypes or len(crit.attribute) < 1: raise ValueError, "query %s has an invalid attribute" % crit.attribute
		crit.operator = __find_operator_factory(query["operator"])()
		crit.value = query["value"]
		return crit
	
	else: raise ValueError, "Invalid query %s passed to normalize_expression" % query

def normalize_query(query):
	"""Do not use this =)"""
	# returns a Query object
	if not __is_query_expression(query): raise ValueError, "query %s is invalid"%query
	nq = Query()
	nq.expression = normalize_expression(query["expression"])
	if query.has_key("infospaces"):
		if not type(query["infospaces"]) is list: raise ValueError, "provided infospaces are not a list"
		for a in query["infospaces"]:
			if not type(a) in StringTypes or len(a) < 1:
				raise ValueError, "invalid infospace name %s", repr(a)
		nq.infospaces = [ a for a in query["infospaces"] ]
	return nq

class Query:
	"""Do not use this =)"""
	infospaces = []
	expression = None
		
	def __str__(self):
		if len(self.infospaces) == 0: a = "all infospaces"
		else: a = "infospaces %s"% ", ".join(self.infospaces)
		return "Query for %s with %s" % (a,self.expression)

class Expression: pass

class Operator:
	"""Abstract operator representation.  Use one of the subclasses."""
	oprepr = "unknown"
	def __str__(self): return self.oprepr
	def __eq__(self,obj):
		return self.__class__ is obj.__class__

class Criterion(Expression):
	"""Represents a search criterion.  Pass an attribute (string), an operator
	(Operator instance) and a value (one of the basic Python types)."""
	attribute = None
	operator = None
	value = None
	def __str__(self): return "%s %s %s" % (self.attribute,self.operator,repr(self.value))
	def __eq__(self,obj):
		return obj.__class__ == self.__class__ and obj.attribute == self.attribute and obj.operator == self.operator and obj.value == self.value

	def __init__(self,attribute=None,operator=None,value=None):
		if attribute: self.attribute = attribute
		if operator:
			assert isinstance(operator,Operator)
			self.operator = operator
		if value: self.value = value
		

class Equals(Operator):
	"""Searches for exact matches\nTextual representation: ="""
	oprepr = "="
class NL(Operator):
	"""Performs a case-insensitive natural language query on the chosen attribute\nTextual representation: ?="""
	oprepr = "=?"
class Contains(Operator):
	"""Searches for substring matches\nTextual representation: contains"""
	oprepr = "contains"
class IsGreaterThan(Operator):
	"""Searches for values that are larger or lexically after\nTextual representation: >"""
	oprepr = ">"
class IsLessThan(Operator):
	"""Searches for values that are smaller or lexically before\nTextual representation: <"""
	oprepr = "<"
class IsEqualOrGreaterThan(Operator):
	"""Searches for values that are equal or larger or lexically the same or after\nTextual representation: >="""
	oprepr = ">="
class IsEqualOrLessThan(Operator):
	"""Searches for values that are equal or smaller or lexically the same or before\nTextual representation: <="""
	oprepr = "<="
class MatchesRegExp(Operator):
	oprepr = "=~"
	"""Searches for values that are match the given expression\nTextual representation: =~"""
class MatchesGlobExp(Operator):
	"""Searches for values that match a globbing expression\nTextual representation: =*"""
	oprepr = "=*"
# class IEquals(Operator): oprepr = "i="
# class IContains(Operator): oprepr = "ihas"
# class IMatchesRegExp(Operator): oprepr = "i=~"
# class IMatchesGlobExp(Operator): oprepr = "i=*"
class IsBelow(Operator):
	"""Searches for paths in the given path\nTextual representation: /"""
	oprepr = "/" # for path queries
class IsOrIsBelow(Operator):
	"""Searches for paths equal to or in the given path\nTextual representation: /*"""
	oprepr = "/=" # for path queries

operators = [
Equals
,Contains
,NL
,IsGreaterThan
,IsLessThan
,IsEqualOrGreaterThan
,IsEqualOrLessThan
,MatchesRegExp
,MatchesGlobExp
# ,IEquals
# ,IContains
# ,IMatchesRegExp
# ,IMatchesGlobExp
,IsBelow
,IsOrIsBelow
]

def __find_operator_factory(string):
	for a in operators:
		if string == a.oprepr: return a
	raise ValueError, "unknown operator %s" % repr(string)
 
class Aggregation(Expression):
	"""Abstract representation of an aggregation. Use one of the subclasses."""
	def __init__(self,list_of_expressions):
		if not type(list_of_expressions) is list: raise ValueError, "Aggregation expressions need a list of subexpressions"
		if not len(list_of_expressions) > 0: raise ValueError, "Aggregation expressions need at least one subexpression"
		for a in list_of_expressions:
			assert isinstance(a,Expression)
		self.expressions = list_of_expressions
	
	expressions = []

class Or(Aggregation):
	"""Represents an OR aggregation."""
	def __str__(self):
		return "( %s )" % " or ".join([ "%s"%a for a in self.expressions ])
		
class And(Aggregation):
	"""Represents an AND aggregation."""
	def __str__(self):
		return "( %s )" % " and ".join([ "%s"%a for a in self.expressions ])

class Not(Aggregation):
	"""Represents an OR exclusion. Unimplemented for now."""
	def __init__(self):
		raise NotImplementedError
		
def __optimize_expression(exp):
	assert isinstance(exp,Criterion) or isinstance(exp,Aggregation)
	if isinstance(exp,Criterion):
		return exp # only a criterion, nothing to optimize here, move along!
	if isinstance(exp,Aggregation):
		exp.expressions = [ __optimize_expression(e) for e in exp.expressions ]
		ors = [ a for a in exp.expressions if isinstance(a,Or) ]
		ands = [ a for a in exp.expressions if isinstance(a,And) ]
		criteria = [ a for a in exp.expressions if isinstance(a,Criterion) ]
# 		if len(ors) == 0 and len(ands) == 0: return exp # they are all criterions, nothing to optimize here, move along!
		newexps = []
		if isinstance(exp,Or):
			sameagg = ors; differentagg = ands
		elif isinstance(exp,And):
			sameagg = ands; differentagg = ors
		for a in sameagg: newexps.extend([b for b in a.expressions])
		newexps.extend([a for a in differentagg])
		newexps.extend(criteria)
		exps = []
		for a in newexps:
			if not a in exps: exps.append(a)
		exp.expressions = exps
		if len(exp.expressions) < 1: raise ValueError, "Expression %s evaluated to an empty expression"%exp
		if len(exp.expressions) == 1: return exp.expressions.pop()
		return exp

def __simplify_aggregations(expression):
	assert isinstance(expression,Criterion) or isinstance(expression,Aggregation)
	if isinstance(expression,Criterion):
		return expression
	elif isinstance(expression,Aggregation):
		if len(expression.expressions) == 1:
			return __simplify_aggregations(expression.expressions[0])
		else:
			newexpressions = []
			for exp in expression.expressions:
				newexpressions.append(__simplify_aggregations(exp))
			expression.expressions = newexpressions
			return expression
				
def optimize(expression):
	"""Optimizes any expression.  The catalog automatically applies this
	when searching."""
	return __optimize_expression(__simplify_aggregations(expression))
