#!/usr/bin/env python
# -*- coding: utf-8 -*-

import random
import string
import json
import socket
import time
import Alfons.Crypt
import traceback
import threading
import logging
import os
import sys

ACP_VERSION = "0.6"
ATP_VERSION = "0.2"

deviceSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

info = {}
requests = {}

path = os.path.dirname(os.path.abspath(sys.argv[0]))

log = logging.getLogger("alfons")

logging.basicConfig(filename=path + "/" + os.path.splitext(os.path.basename(os.path.abspath(sys.argv[0])))[0] + ".log",level=logging.DEBUG, format="%(asctime)s %(name)s %(levelname)s %(filename)s line %(lineno)d %(funcName)s \t %(message)s", datefmt="%Y-%m-%d %H:%M:%S %z")

def request(command, dest, data, headers={}, requestId=None,  **kwargs):
	"Send a request with the command, data and headers to the destination"

	headers["sender"] = info["device_id"]
	headers["destination"] = dest
	request = AlfonsRequest().setFromComponents(command, headers, data, AlfonsRequest.createRequestId())

	return requestRequestObject(request, **kwargs)

def requestRequestObject(request, **kwargs):
	requestId = request.requestId
	
	if kwargs.get("transportCallback", False):
		request.headers["data-transfer"] = "expected"
		
		requests[requestId + "_atp"] = {}
		requests[requestId + "_atp"]["callback"] = kwargs.get("transportCallback")
		requests[requestId + "_atp"]["timestamp"] = int(time.time())

		atpClient = Alfons.setUpATPClient()
		def getATPData():
			response = AlfonsResponse().setFromString(Alfons.recvFullATP(atpClient)[0])
			requests[requestId + "_atp"]["callback"](response)
	
		atpThread = threading.Thread(target=getATPData)
		atpThread.daemon = True
		atpThread.start()

	sendToServer(request.export())
	
	if kwargs.get("callback", False):
		if kwargs.get("callback") != "return":
			requests[requestId] = {}
			requests[requestId]["callback"] = kwargs.get("callback")
			requests[requestId]["timestamp"] = int(time.time())
		
		return listen(None, forId=requestId)

	return request

def sendToServer(s):
	"Encrypt a string with the server public key and send it to the server"
	s = Alfons.Crypt.encrypt(s, info["alfons_key"])
	deviceSocket.sendall(s)

def findServer(**kwargs):
	"Find the server and register"
	global info
	
	discoverRequestString = AlfonsRequest().setFromComponents("discover", {"destination": "alfons"}, {}, AlfonsRequest.createRequestId()).export()

	broadcastSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	broadcastSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	broadcastSocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

	def broadcast():
		stopBroadcast = False
		while not stopBroadcast:
			try:
				broadcastSocket.sendto(discoverRequestString, (kwargs.get("ip", "255.255.255.255"), 27373))
			except:
				stopBroadcast = True
				break
			time.sleep(3)

	broadcastThread = threading.Thread(target=broadcast)
	broadcastThread.daemon = True
	broadcastThread.start()
	
	if kwargs.get("receiveMultiple", False):
		servers = []

		def receive(servers):
			while True:
				try:
					data, addr = broadcastSocket.recvfrom(2028)
					servers += [(data, addr)]
				except:
					break

		receiveThread = threading.Thread(target=receive, args=(servers, ))
		receiveThread.daemon = True
		receiveThread.start()

		time.sleep(3)
		broadcastSocket.close()
		items = []

		for data, addr in servers:
			body = AlfonsResponse().setFromString(data).body
			body["id"] = body["public_key"][71:112].replace("\n", "")
			body["ip"] = "%s:%s" % addr
			items.append(body)

		return items 
	else:
		data, addr = broadcastSocket.recvfrom(2028)
		broadcastSocket.close()
		response = AlfonsResponse().setFromString(data)
		return (addr[0], Alfons.Crypt.AlfonsRSA.importKey(response.body["public_key"]))

def register():
	body = {"id": info["device_id"], "name": info["name"], "description": info["description"], "public_key": info["public_key"].exportKey()}
	return request("register", "alfons", body, callback="return")

def connect(ip, port = 27373):
	"Connect to the ip"
	global info

	info["ip"] = ip
	info["port"] = port

	deviceSocket.connect((ip, port))

def close():
	"Close the connection to the server"
	deviceSocket.close()

def createErrorResponse(message, requestId):
	"Create a response string with an error message"
	return AlfonsResponse().setFromComponents({"destination": info["device_id"], "sender": "alfons", "success": False, "message": message}, {}, requestId)

def listen(run, **kwargs):
	"Listen for a message from the server"
	if kwargs.get("forId", False):
		deviceSocket.settimeout(kwargs.get("timeout", 7))
	else:
		deviceSocket.settimeout(None)

	while True:
		try:
			data, addr = deviceSocket.recvfrom(2028)
		except socket.timeout: # Timeout will only occur if 'forId' is set due to 'deviceSocket.settimeout(kwargs.get("timeout", 7))'
			requestId = kwargs.get("forId")
			response = createErrorResponse("Timed out", requestId)
			requests[requestId]["callback"](response)
			return response

		if not data.startswith("ACP/" + ACP_VERSION):
			(data, signature) = Alfons.Crypt.decrypt(data)
			if not Alfons.Crypt.AlfonsRSA.verify(data, signature, info["alfons_key"]):
				print "Not valid"
				continue

		r = decodeR(data)
		
		if not r:
			continue

		if r["type"] == "request":
			if not run is None:
				request = AlfonsRequest().setFromComponents(r["command"], r["headers"], r["body"], r["request_id"])

				try:
					d = run(request)
					if d is None:
						log.error("Didn't get any data from command (%s)" % request.command)
					data = d if d is not None else ({"success": False, "message": "Didn't get any data from command"}, {})
					if not isinstance(d, tuple):
						data = (data, {})
				except:
					log.critical("Error executing command (%s) on IoT" % request.command, exc_info=1)
					data = ({"success": False, "message": "Error executing command on IoT"}, {})
				
				if not "destination" in data[0]:
					data[0]["destination"] = request.headers["sender"]
				response = AlfonsResponse().setFromComponents(data[0], data[1], request.requestId)
				
				sendToServer(response.export())
			else:
				response = AlfonsResponse().setFromComponents({"sender": info["device_id"], "destination": r["headers"]["sender"], "success": False, "message": "This device does not take any commands (right now)"}, {}, r["request_id"])
				sendToServer(response.export())
		else:
			response = AlfonsResponse().setFromComponents(r["headers"], r["body"], r["request_id"])
			forId = kwargs.get("forId", False)
			if forId == response.requestId:
				if response.requestId in requests:
					requests[response.requestId]["callback"](response)

				return response

def eradicate():
	"Send an eradicate request to the server and listen for the response"
	log.info("Eradicating")
	r = request("eradicate", "alfons", {"id": info["device_id"]})
	return listen(None, forId=r.requestId)

class AlfonsRequest:
	"An object holding all information about a request"
	address = ("", 0)

	@staticmethod
	def createRequestId():
		"Generate a request id"
		return "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20))

	def setFromComponents(self, command, headers, body, requestId, protocol="ACP"):
		"Set the response info from components"
		self.command = command
		self.headers = headers
		self.body = body
		self.requestId = requestId
		self.protocol = protocol

		return self

	def setFromString(self, s):
		"Set the response info from a strign"
		r = decodeR(s)
		self.setFromComponents(r["command"], r["headers"], r["body"], r["request_id"], r["protocol"])

		return self

	def export(self):
		"Export the object to a string"
		
		requestString = "ACP/" + ACP_VERSION + " " + self.command + " " + self.requestId

		if not "sender" in self.headers:
			self.headers["sender"] = Alfons.info["device_id"]

		for h in self.headers:
			requestString += "\n" + h.capitalize() + ": " + self.headers[h]

		requestString += "\n\n" + json.dumps(self.body)
		
		return requestString

class AlfonsResponse:
	"An object holding all info about a response"
	
	def __init__(self):
		self.headers = {}
		self.body = {}
		self.requestId = ""

	def setFromComponents(self, headers, body, requestId, protocol="ACP"):
		"Set info from components"
		self.headers = headers
		self.body = body
		self.requestId = requestId
		self.protocol = protocol

		return self

	def setFromString(self, s):
		"Set info from a string"
		r = decodeR(s)
		
		if not "sender" in self.headers:
			self.headers["sender"] = Alfons.info["device_id"]

		key = "body" if not "content" in r else "content"
		self.setFromComponents(r["headers"], r[key], r["request_id"], r["protocol"])

		return self

	def export(self):
		"Export the object to a string"

		version = ACP_VERSION if self.protocol == "ACP" else ATP_VERSION
		responseString = self.protocol + "/" + version + " " + self.requestId

		if not "sender" in self.headers:
			self.headers["sender"] = Alfons.info["device_id"]

		for h in self.headers:
			responseString += "\n" + h.capitalize() + ": " + str(self.headers[h])

		responseString += "\n\n" + json.dumps(self.body)

		return responseString

def decodeR(r, sender = None):
	"Decode a request or response to a dict"
	requestId = ""
	headers = {}
	body = {}

	response = {}
	
	try:
		parts = r.split("\n\n", 1)
		sHeaders = parts[0].split("\n")
		sBody = parts[1]
		topRow = sHeaders.pop(0).split(" ")
		
		# Remove the first object (protocol), and get the last (id)
		topRow.pop(0)
		requestId = topRow.pop()
		
		response["request_id"] = requestId

		# If it's a request there will be a command
		if len(topRow) == 1:
			response["type"] = "request"
			response["command"] = topRow.pop()
		else:
			response["type"] = "response"

		# Convert the headers to a dict
		for h in sHeaders:
			parts = h.split(": ")
			key = parts[0].lower()
			value = parts[1]

			if value == "True":
				value = True
			elif value == "False":
				value = False

			headers[key] = value
		
		response["headers"] = headers
		
		if r.startswith("ACP/"):
			body = json.loads(sBody)
			response["body"] = body
			response["protocol"] = "ACP"
		elif r.startswith("ATP/"):
			response["content"] = sBody
			response["protocol"] = "ATP"
	except:
		print "Not valid ACP/" + ACP_VERSION
		return {}

	return response

def readSettings(path, defaultSettings = {}):
	with open(path + "/settings.json", "r+") as f:
		try:
			settings = json.load(f)
		except:
			settings = {}

	for k in defaultSettings:
		if k not in settings:
			settings[k] = defaultSettings[k]

	for s in settings:
		Alfons.info[s] = settings[s]

def writeSettings(path, settingsKeys):
	settings = {}

	for k in settingsKeys:
		settings[k] = Alfons.info[k]

	with open(path + "/settings.json", "w+") as f:
		json.dump(settings, f)

def recvFullATP(connection):
	data = ""

	# while (notEncryped and not receivedFullContentLength) or (encrypted and not receivedEnd)
	while (data.startswith("ATP/") and not getATPContentLength(data) == getATPContentLengthHeader(data)) or (not data.startswith("ATP/") and not data.endswith("\n\n")):
		data += connection.recv(1)

	if not data.startswith("ATP/"):
		return Alfons.Crypt.decrypt(data)

	return (data, None)

def setUpATPClient(destination = None):
	"Set up an ATP client with defined mode. 'recv' to receive and 'send' to send"
	client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	ip = "" if not "ip" in info else info["ip"]
	client.connect((ip, info["port"]))
	
	if destination:
		initString = "ATP/" + ATP_VERSION + " initsend _\nSender: " + info["device_id"] + "\nDestination: " + destination + "\nContent-length: 0\n\n"
	else:
		initString = "ATP/" + ATP_VERSION + " initrecv _\nSender: " + info["device_id"] + "\nContent-length: 0\n\n"

	encInitString = Alfons.Crypt.encrypt(initString, Alfons.info["alfons_key"])
	client.sendall(encInitString + "\n\n")

	return client

def transportString(s, request):
	client = setUpATPClient(request.headers["sender"])
	
	transferString = "ATP/" + ATP_VERSION + " transfer " + request.requestId + " \nSender: " + info["device_id"] + "\nContent-length: " + str(len(s)) + "\n\n" + s
	encTransferString = Alfons.Crypt.encrypt(transferString, Alfons.info["alfons_key"])
	client.sendall(encTransferString + "\n\n")
	
	client.close()

def getATPContentLengthHeader(s):
	lines = s.split("\n")
	i = 0
	for l in lines:
		l2 = l.split(": ")
		if l2[0].lower() == "content-length" and len(lines) > i + 1:
			return int(l2[1])
		i+= 1

def getATPContentLength(s):
	parts = s.split("\n\n")

	if len(parts) >= 2:
		return len(parts[1])

	return -1