#! /usr/bin/python3

#load the libraries
import sys
import json
import array
import random
import numpy as np
import math

#the number of users created yet (used for the userids)
userCounter = 0

#the object representing normal users
class User(object):

	#the creation of the object
	#parameter:
	#	attributes:     list of attributes for the user
	#	fingerprinters: a list of fingerprinters to visit, the first one is for statistics
	#HACK: fingerprinters is not realy a list since it has only one element -_-'
	def __init__(self,attributes,fingerprinters):
		#list of attributes for the user
		self.attributes            = attributes
		#the fingerprinter to visit
		self.fingerprinters        = fingerprinters

		#set the userid
		global userCounter
		userCounter  = userCounter + 1

		#set the userid
		self.userId  = userCounter

	#the callback function for the main simulation
	def tick(self):
		self.action()

	#do some tasks
	def action(self):
		self.browse()
	
	#visit the fingerprinter
	def browse(self):
		#HACK: [0] since fingerprinters is not realy a list
		self.fingerprinters[0].visitedBy(self)

	#a empty function for randomization calls
	def randomize(self):
		pass

#the object representing a user with a randomsatiser
class RandomUser(User):

	#the creation of the object
	#parameter:
	#	attributes: the list of attributes to meassure to obtain a fingerprint
	#	fingerprinters: a list of fingerprinters to visit, the first one is for statistics
	#	notfixed: a list of attribute indexes marking these attributes as changeable
	def __init__(self,attributes,fingerprinters,notfixed):
		super().__init__(attributes,fingerprinters)

		#the attributes which are changeable
		self.notfixed = notfixed

	#randomize the fingerprint
	def randomize(self):
		#randomize each attribute if changeble and a random chance was considered
		counter = 0
		while counter < 10:
			if counter not in self.notfixed:
				counter += 1
				continue
			
			if random.random() < float(sys.argv[4]):
				self.attributes[counter] = random.choice(range(0,2000))

			counter += 1

#the object representing a fingerprintingscript
class Fingerprinter(object):
	
	#the creation of the object
	#parameter:
	#	 fingerprintedAttributes: the list of attributes to meassure to obtain a fingerprint
	def __init__(self,fingerprintedAttributes):
		#the list of attributes to meassure to obtain a fingerprint
		self.fingerprintedAttributes = fingerprintedAttributes

		#the fingerprints meassured
		self.messuredFingerprints = []

	#the function for beeing visited by an user
	#parameter:
	#       user: the user visiting
	def visitedBy(self,user):
		self.messuredFingerprints.append([user.userId,self.extractFingerprint(user)])

	#reset the fingerprinterstate
	def reset(self):
		#clear the meassured fingerprints
		self.messuredFingerprints = []

	#measure the fingerprint for a user
	#parameter:
	#	user: the user to get the fingerprint from
	def extractFingerprint(self,user):
		result = []
		for index in self.fingerprintedAttributes:
			result.append(user.attributes[index])
		return result

#the fingerprinter for detecting randomization
class AntiRandomFingerprinter(Fingerprinter):

	#the creation of the object
	#parameter:
	#       fingerprintedAttributes: the list of attributes to meassure to obtain a fingerprint
	def __init__(self,fingerprintedAttributes):
		#call superclass constructor
		super().__init__(fingerprintedAttributes)

		#the fingerprints meassured for each user
		self.userFingerprints = {}

	#the function for beeing visited by an user
	#parameter:
	#       user: the user visiting
	def visitedBy(self,user):
		#prepare a container for this user if needed
		if not user.userId in self.userFingerprints:
			self.userFingerprints[user.userId]=[]

		#add the fingerprint to the fingerprints measured for this user
		self.userFingerprints[user.userId].append(self.extractFingerprint(user))

#the actual simulation of the fingerprinting process
class Simulation(object):
	#the creation of the object
	#parameter:
	#       numUsers:       the number of users to simulate
	def __init__(self,numUsers=1):
		#save the number of users
		self.numUsers = numUsers

		#the fingerprinter for detecting the randomization
		self.__randomFingerprinter = AntiRandomFingerprinter([0,1,2,3,4,5,6,7,8,9])

		#create the users
		self.__users = []
		for i in range(0,self.numUsers):
			self.__users.append(self.__generateUser())

		#the container for the result
		self.__result = []

	#run the simulation
	#parameter:
	#	ticks:  the number of ticks to simulate
	def run(self,ticks=1):
		#do all ticks
		for i in range(0,ticks):
			print("tick "+str(i+1))

			#do the tick on all users
			for user in self.__users:
				user.tick()
		
			#calculate the statistics for this tick
			self.middleStatics()

			#forge the best fingerprint
			if i < int(sys.argv[3])-1:
				counter = 0
				for user in self.__users:
					user.randomize()
					counter += 1

	#calculate the statistics for a tick
	def middleStatics(self):
		import math

		#check how many fingerprints were detected correctly
		correct = 0
		for user in self.__users:
			#check wich indices apeared to be changable
			tmpNonFixed = []
			counter = 0
			while counter < 10:
				comp = None
				for fingerprint in self.__randomFingerprinter.userFingerprints[user.userId]:
					if comp == None:
						comp = fingerprint[counter]
					elif not comp == fingerprint[counter]:
						tmpNonFixed.append(counter)
						break
				counter += 1

			#compare detected changable attributes against the real value
			if tmpNonFixed == user.notfixed:
				correct += 1

		#get the average number of different fingerprints each user had
		numFingerprints = 0
		for user in self.__users:
			counterArr = {}
			for fingerprint in self.__randomFingerprinter.userFingerprints[user.userId]:
				if not str(fingerprint) in counterArr:
					counterArr[str(fingerprint)] = 0
		
				counterArr[str(fingerprint)] += 1
			numFingerprints += len(counterArr.values())
		numFingerprints = numFingerprints/len(self.__users)
				
		#the result for this tick
		roundResult = {}
		
		#add the data to the result
		roundResult["correct"] = correct
		roundResult["num"] = numFingerprints

		#print the temporary result
		print(roundResult)

		#add the result for the tick to the general result
		self.__result.append(roundResult)

	#end the simulation
	#return:
	#	the data collected in the simulation
	def finish(self):
		print("finished")
		return self.__result

	#generate a new user
	#return:
	#	the new user
	def __generateUser(self):
		#the propabilities for generating users
		highProbability = 0.69
		lowProbability = 0.36

		#generate the fingerprint for the new user
		attributes = {}
		counter = 0
		#choose a distribution for unique users or big anonymity sets
		if random.random() < 0.1:
			while counter < 10:
				attributes[counter] = np.random.geometric(highProbability)+10
				counter += 1
		else:
			while counter < 10:
				attributes[counter] = np.random.geometric(lowProbability)+10
				counter += 1

		#the fingerprinter to visit
		fingerprinters = [self.__randomFingerprinter]

		#generate randomizing user
		notfixed = []
		counter = 0
		#make some attributes changeble
		while counter < 10:
			if random.random() < float(sys.argv[5]):
				notfixed.append(counter)

			counter += 1

		#return the user
		return RandomUser(attributes,fingerprinters,notfixed)

#seed the random generators with a fixed seed
random.seed(sys.argv[7])
np.random.seed(int(sys.argv[7]))

#create the folder to write the data into
import os
try:
        os.stat(sys.argv[6])
except:
        os.mkdir(sys.argv[6])

try:
        os.stat(sys.argv[6]+"/data/")
except:
        os.mkdir(sys.argv[6]+"/data/")

#container for the results
allData = []
finalResult = []

#set the number of runs to do
numRuns = int(sys.argv[1])

#set up dummies for each part of the results
counter = 0
while counter < int(sys.argv[3]):
	tmpResult = {}
	tmpResult["correct"]         = 0
	tmpResult["num"]         = 0
	tmpResult["tick"]           = 0

	finalResult.append(tmpResult)
	
	counter += 1

#run the simulation and average the results
counter = 0
while (counter < numRuns):
	counter += 1

	#run the simulation and get its result
	simulation = Simulation(numUsers=int(sys.argv[2]))
	simulation.run(int(sys.argv[3]))
	result = simulation.finish()

	#add the round results to the complete result
	allData.append(result)

	#print some eyecandy
	print("")
	print("run: "+str(counter))
	print(result)
	print("")

	#add the round results to the complete result
	counter2 = 0
	while counter2 < int(sys.argv[3]):
		finalResult[counter2]["correct"]        += result[counter2]["correct"]
		finalResult[counter2]["num"]        += result[counter2]["num"]
		finalResult[counter2]["tick"]           += counter2+1

		counter2 += 1

#finish calculating the average
counter = 0
while counter < int(sys.argv[3]):
	finalResult[counter]["correct"]         /= numRuns
	finalResult[counter]["num"]         /= numRuns
	finalResult[counter]["tick"]           /= numRuns

	counter += 1

#calculate the variance
varianz = []
counter = 0
while counter < int(sys.argv[3]):
	tmpResult = {}
	
	tmpResult["correct"]         = 0
	tmpResult["num"]             = 0
	tmpResult["tick"]            = 0

	varianz.append(tmpResult)
	
	counter += 1

counter = 0
while (counter < numRuns):
	
	counter2 = 0
	while counter2 < int(sys.argv[3]):
		varianz[counter2]["correct"] += math.pow(allData[counter][counter2]["correct"]-finalResult[counter2]["correct"],2)
		varianz[counter2]["num"] += math.pow(allData[counter][counter2]["num"]-finalResult[counter2]["num"],2)

		counter2 += 1

	counter += 1

counter = 0
while counter < int(sys.argv[3]):
	varianz[counter]["correct"]     /= numRuns
	varianz[counter]["num"]     /= numRuns

	counter += 1

#calculate the coefficient of variation
varianzKoeff = []

counter = 0
while counter < int(sys.argv[3]):
	tmpResult = {}
	try:
		tmpResult["correct"]          = math.sqrt(varianz[counter]["correct"])/finalResult[counter]["correct"]
	except:
		tmpResult["correct"]          = float('nan')
	try:
		tmpResult["num"]          = math.sqrt(varianz[counter]["num"])/finalResult[counter]["num"]
	except:
		tmpResult["num"]          = float('nan')

	varianzKoeff.append(tmpResult)

	counter += 1

#save the detailed result
data_dump = json.dumps(allData)
file = open(sys.argv[6]+"/condensedData","w")
file.write(data_dump)
file.close()

#add the variance and coeffient of variation
counter = 0
while counter < int(sys.argv[3]):
        finalResult[counter]["varianz"]          = varianz[counter]
        finalResult[counter]["varKoeff"]         = varianzKoeff[counter]

        counter += 1

#print the result
print(finalResult)

#save the averaged result
result_dump = json.dumps(finalResult)
file = open(sys.argv[6]+"/results","w")
file.write(result_dump)
file.close()
