import unittest
import random
import pyfmi.testcase
import operator
import string
import math
from itertools import *


points = 5

def module_game(m1, m2):
	return (assess(m1, m2), assess(m2, m1))

def assess(mod, other):
	return sum((
		end3_funcs(mod),
		avg_lists(mod, other),
		commutatives(mod),
		3*monty_python_dicts(mod),
	))

def valid_attributes(mod):
	return (attr for attr in dir(mod) if not attr.startswith('_'))

def end3_funcs(mod):
	func_names = [attr for attr in valid_attributes(mod) if callable(getattr(mod, attr))]
	count = 0
	for func in func_names:
		if len(func) < 3: continue
		start = func[:3]
		if any((otherfunc.endswith(start) for otherfunc in func_names if func != otherfunc)):
			count += 1
	return count

def avg_lists(mod, other):
	other_ints = [getattr(other, attr) for attr in valid_attributes(other)
			if isinstance(getattr(other, attr), (int, long))]
	other_sum, other_len = sum(other_ints), len(other_ints)
	mod_lists = (getattr(mod, attr) for attr in valid_attributes(mod)
			if isinstance(getattr(mod, attr), list))
	count = 0
	square = lambda x: x*x
	for l in mod_lists:
		jingled_items = [sum(imap(square, tup)) for tup in l]
		jingled_sum, jingled_len = sum(jingled_items), len(jingled_items)
		# don't use floats
		# we know that always both the sum and the len are non-negative
		if jingled_sum > 0 and 0 == other_len:
			count += 1
		elif jingled_sum*other_len > jingled_len*other_sum:
			count += 1
	return count

def commutatives(mod):
	funcs = [getattr(mod, attr) for attr in valid_attributes(mod) if callable(getattr(mod, attr))]
	count = 0
	for func in funcs:
		if func(0,0) == 0 and\
				all((func(i, j) == func(j, i) for i in xrange(0, 99) for j in xrange(i+1, 100))):
			count += 1
	return count

def monty_python_dicts(mod):
	WANTED = set(['chapman', 'cleese', 'gilliam', 'idle', 'jones', 'palin'])
	dicts = [getattr(mod, attr) for attr in valid_attributes(mod) if isinstance(getattr(mod, attr), dict)]
	count = 0
	# DSU, DSU
	def make_decorate(dict_):
		def decorate(key):
			value = dict_[key]
			svalue = str(value).lstrip('-')
			return (len(svalue), math.tan(math.radians(value)))
		return decorate

	for d in dicts:
		ordered = sorted(d, key=make_decorate(d), reverse=True)
		if set(imap(string.lower, ordered[:6])) == WANTED:
			count += 1
	return count


class ProblemTests(pyfmi.testcase.TestCase):
	def test_1_end3_funcs_simple(self):
		m1 = self.make_mod()
		m2 = self.make_mod()
		self.add_funcs(m1, 'tiririram', 'robustotroniX');
		self.add_funcs(m2, 'habobUTU', 'UTUmana', 'UTUdaba');
		self.assert_game(m1, m2, (0, 2))

	def test_2_end3_funcs_complex(self):
		m1 = self.make_mod()
		m2 = self.make_mod()
		self.add_funcs(m1,
				'roy_boss',
				'destroy',
				'roy_keen',
				'pinkoRroy', # more than one end with one beginning
				'boyish', # non-function, see m1.eenboy below
				);
		m1.eenboy = ('Pinko', 'the', 'Pink', 'panther')
		self.add_funcs(m2,
				'ri', 'rri', # just 2 chars
				'saZub', 'xxsaz', # different case
				'_baba', 'abaduc', # underscore
				'Tru', 'xTru', # 3-char func, ok
		)
		self.assert_game(m1, m2, (2, 1))	

	def test_3_avg_lists_simple(self):
		m1 = self.make_mod()
		m2 = self.make_mod()
		self.add_ints(m1, -33, 77, 31) # 75/3 -> 25
		m2.l0 = [(5,), (5,), (5,), (), (6, 8), (), ()] # 25,25,25,0,100,0,0 -> 25 => equal
		m2.l1 = [(), ()] # 0, less
		m2.l2 = [(10,), (213,), (3, 4, 5, 88), (-18, -3, 44)] # uncountable ;)
		self.assert_game(m1, m2, (0, 1))

	def test_4_avg_lists_tricky(self):
		# only zeros 
		m1 = self.make_mod()
		m1.guga = []
		m1.gugu = [()]
		m1.i = 0
		m2 = self.make_mod()
		m2.l0 = [(), (), (), (), ()]
		self.assert_game(m1, m2, (0, 0))
		# negative, zero
		m1 = self.make_mod()
		m2 = self.make_mod()
		self.add_ints(m1, -88, 2, -123, 22, 0, 0, 0, -77) # -33.125, every list in m2 should suffice
		self.add_ints(m2, 0, -3, 3) # 0, quite every list in m1 should suffice
		m1.l = []
		m1.l0 = [()]
		m1.l1 = [(), (), (), (), (), (-4,)]
		m2.l = [(), (7,), (9,)]
		self.assert_game(m1, m2, (1, 1))
		# non-integer averages
		m1 = self.make_mod()
		m2 = self.make_mod()
		self.add_ints(m1, 21, 21, 21, 22) #  21.25, 21 if int div used
		m2.l = [(8,), (), ()] # 21.(3), 21 if int div used
		self.assert_game(m1, m2, (0, 1))
		#TODO: bigger test, who knows what could go wrong :)	

	def test_5_comm_simple(self):
		m1 = self.make_mod()
		m2 = self.make_mod()
		m1.a = operator.add
		m1.b = operator.mul
		m1.c = self.make_func()
		m2.d = lambda x,y: 0
		m2.e = self.make_func()
		self.assert_game(m1, m2, (2, 1))
		
	def test6_comm_complex(self):
		m1 = self.make_mod()
		m2 = self.make_mod()
		m1.x = 77
		m1.a = lambda x,y: x+y if x != 98 and y != 99 else -2 # nyyah
		m1.b = lambda x,y: x*y if x != 0 and y != 99 else 666
		m2.c = lambda x,y: x*y + y * x # 0:1
		m2.d = lambda x,y: 0 if x != 99 and y != 98 else 42
		m2.e = lambda x,y: 1 if x != 0 and y != 0 else 0 # 0:2
		m2.f = lambda x,y: x+y if x!=50 and y!=47 else 7324
		m2.g = lambda x,y: x+y if x!=99 and y!=0 else -11
		self.assert_game(m1, m2, (0, 2))

	def test7_sort_simple(self):
		m1 = self.make_mod()
		m2 = self.make_mod()
		# no tan or case-insensitivity needed here
		m1.d = {'Chapman': 1111111, 'Cleese': 111111, 'Gilliam': 11111, 'Idle': 1111, 'Palin': 111, 'Jones': 11, 'Pinko': 1}
		m1.baba = 8
		self.assert_game(m1, m2, (3, 0))
		
	def test_8_sort_complex(self):
		if hasattr(self.user, 'fail8'): raise "Infinite loop in digits counting!"
		m1 = self.make_mod()
		m2 = self.make_mod()
		m1.a = {}
		m2.b = {'ChApMaN': -123213, 'cleese': 3, 'GiLLiam': 88, 'iDLE': 3.14, 'PalIn': 111, 'joNes': 0} # case
		m2.c = {'ChApMaN': 77, 'cleese': 88, 'GiLLiam': 99, 'iDLE': 33, 'PalIn': 21, 'joNes': 11, 'JONES': 92}
		m2.d = {'hey': -2, 'ChApMaN': 77, 'cleese': 88, 'GiLLiam': 99, 'iDLE': 33, 'PalIn': 21, 'joNes': 11, 'JONES': 92, 'bazuki': 88}
		self.assert_game(m1, m2, (0, 6))

	def assert_game(self, m1, m2, res):
		self.assertEqual(self.user.module_game(m1, m2), res)

	@staticmethod
	def make_func():
		"""Makes none-commutative functions"""
		funcs = (
			lambda x,y: x*y+333,
			lambda x,y: -3,
			lambda x,y: x**y,
		)
		return random.choice(funcs)

	@staticmethod
	def add_funcs(mod, *names):
		for name in names:
			setattr(mod, name, ProblemTests.make_func())

	@staticmethod
	def add_ints(mod, *values):
		for (i, value) in enumerate(values):
			setattr(mod, 'int%d' % i, value)

	@staticmethod
	def make_mod():
		import imp
		return imp.new_module('m_'+str(random.randint(0, 2**31)))

if __name__ == "__main__":
	ProblemTests.user_filename = 'test.py';
	suite = unittest.defaultTestLoader.loadTestsFromTestCase(ProblemTests)
	res = unittest.TestResult()
	suite.run(res)
	runner  = unittest.TextTestRunner()
	runner.run(suite)
