import sqlite3

from math import tanh

class SearchNet(object):
	def __init__(self, dbname):
		self.con = sqlite3.connect(dbname)


	def __del__(self):
		self.con.close()

	def maketables(self):
		 self.con.execute('create table hiddennode(create_key)')
		 self.con.execute('create table wordhidden(fromid, toid, strength)')
		 self.con.execute('create table hiddenurl(fromid, toid, strength)')
		 self.con.commit()

	def dtanh(self, y):
		return 1.0 - y*y

	def get_strength(self, fromid, toid, layer):
		if layer == 0:
			table = 'wordhidden'
		else:
			table = 'hiddenurl'

		res = self.con.execute(
			'select strength from %s where fromid=%d and toid=%d' %
			(table, fromid, toid)).fetchone()

		if res is None:
			if layer == 0:
				return -0.2
			if layer == 1:
				return 0

		return res[0]

	def set_strength(self, fromid, toid, layer, strength):
		if layer == 0:
			table = 'wordhidden'
		else:
			table = 'hiddenurl'

		res = self.con.execute(
			'select rowid from %s where fromid=%d and toid=%d' %
			(table, fromid, toid)).fetchone()

		if res is None:
			self.con.execute(
				'insert into %s (fromid, toid, strength) values (%d, %d, %f)' %
				(table, fromid, toid, strength))

		else:
			rowid = res[0]
			self.con.execute(
				'update %s set strength=%f where rowid=%d' %
				(table, strength, rowid))

	def generate_hidden_node(self, wordids, urls):
		if len(wordids) > 3:
			return None

		# check if we already created a node
		# for this set of words
		create_key = '_'.join(sorted([str(wi) for wi in wordids]))

		res = self.con.execute(
			"select rowid from hiddennode where create_key='%s'" %
			create_key).fetchone()

		# if not, create it
		if res == None:
			cur = self.con.execute(
				"insert into hiddennode (create_key) values ('%s')" % 
				create_key)

			hiddenid = cur.lastrowid

			# put in some default weights
			for wordid in wordids:
				self.set_strength(wordid, hiddenid, 0, 1.0/len(wordids))

			for urlid in urls:
				self.set_strength(hiddenid, urlid, 1, 0.1)

			self.con.commit()

	def get_all_hidden_ids(self, wordids, urlids):
		l1 = {}

		for wordid in wordids:
			cur = self.con.execute(
				'select toid from wordhidden where fromid=%d' %
				wordid)

			for row in cur:
				l1[row[0]] = 1

		for urlid in urlids:
			cur = self.con.execute(
				'select fromid from hiddenurl where toid=%d' %
				urlid)

			for row in cur:
				l1[row[0]] = 1

		return l1.keys()

	def setup_network(self, wordids, urlids):
		# value lists
		self.wordids = wordids
		self.hiddenids = self.get_all_hidden_ids(wordids, urlids)
		self.urlids = urlids

		# node outputs
		self.ai = [1.0] * len(self.wordids)
		self.ah = [1.0] * len(self.hiddenids)
		self.ao = [1.0] * len(self.urlids)

		# create weights matrix
		self.wi = [[self.get_strength(wordid, hiddenid, 0)
					for hiddenid in self.hiddenids]
					for wordid in self.wordids]

		self.wo = [[self.get_strength(hiddenid, urlid, 1)
					for urlid in self.urlids]
					for hiddenid in self.hiddenids]

	def feedforward(self):
		# only inputs in first layer are the query words
		for i in range(len(self.wordids)):
			self.ai[i] = 1.0

		# hidden activations
		for j in range(len(self.hiddenids)):
			sum = 0.0

			for i in range(len(self.wordids)):
				sum = sum + self.ai[i] * self.wi[i][j]

			self.ah[j] = tanh(sum)

		# output activations
		for k in range(len(self.urlids)):
			sum = 0.0
			for j in range(len(self.hiddenids)):
				sum = sum + self.ah[j] * self.wo[j][k]

			self.ao[k] = tanh(sum)

		# a copy of output layer after feedforward algo
		return self.ao[:]

	def back_propagate(self, targets, N=0.5):
		# calculate erros for output
		output_deltas = [0.0] * len(self.urlids)
		for k in range(len(self.urlids)):
			error = targets[k] - self.ao[k] # self.ao is the output layer
			output_deltas[k] = self.dtanh(self.ao[k]) * error

		# calculate errors for hidden layer
		hidden_deltas = [0.0] * len(self.hiddenids)
		for j in range(len(self.hiddenids)):
			error = 0.0
			for k in range(len(self.urlids)):
				# change output node by sum of strength of each
				# output link (self.urlids) multuplied by  
				error = error + output_deltas[k]*self.wo[j][k]
			hidden_deltas[j] = self.dtanh(self.ah[j]) * error

		# update output weights
		for j in range(len(self.hiddenids)):
			for k in range(len(self.urlids)):
				change = output_deltas[k] * self.ah[j]
				self.wo[j][k] = self.wo[j][k] + N*change

		# update input weights
		for i in range(len(self.wordids)):
			for j in range(len(self.hiddenids)):
				change = hidden_deltas[j] * self.ai[i]
				self.wi[i][j] = self.wi[i][j] + N*change

	def train_network(self, wordids, urlids, selected_url):
		# generate hidden node if necessary
		self.generate_hidden_node(wordids, urlids)

		self.setup_network(wordids, urlids)
		self.feedforward()
		targets = [0.0] * len(urlids)
		targets[urlids.index(selected_url)] = 1.0
		self.back_propagate(targets)
		self.update_database()

	def update_database(self):
		# set them to db vals
		for i in range(len(self.wordids)):
			for j in range(len(self.hiddenids)):
				self.set_strength(self.wordids[i], self.hiddenids[j], 0, self.wi[i][j])

		for j in range(len(self.hiddenids)):
			for k in range(len(self.urlids)):
				self.set_strength(self.hiddenids[j], self.urlids[k], 1, self.wo[j][k])

		self.con.commit()

	def get_result(self, wordids, urlids):
		self.setup_network(wordids, urlids)
		return self.feedforward()
