#
# Copyright (c) 2016 Christoph Heiss <me@christoph-heiss.me>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

import os
import json
import struct
import threading
import socket
import queue
import tempfile
import base64
import select
from behem0th import utils, log

BLOCK_SIZE = 4096


class Route:
	def handle(self, data, request):
		raise NotImplementedError


	def send(self, data):
		self.handler.send(self.route_name, data)


class FilelistRoute(Route):
	def handle(self, data, request):
		if request.is_client:
			request.client._filelist = data
			request.client._rlock.release()
		else:
			files, events = request.client._merge_filelist(data)
			with request.client._rlock:
				self.send(request.client._filelist)

			for e in events:
				request.queue_event(e)
			for f in files:
				request.queue_file(f[0], f[1])


"""
	{
		"action": "<action>",
		"path": "<relpath-to-file>"
	}
	<action> can be either 'receive' or 'send'

	Payload are base64 encoded chunks (BLOCK_SIZE bytes)
"""
class FileRoute(Route):
	def handle(self, data, request):
		action = data['action']
		path = data['path']

		if action == 'receive':
			tmpf = tempfile.NamedTemporaryFile(delete=False)

			buffer = b''
			for chunk in request.recv():
				buffer += chunk

				if len(buffer) >= BLOCK_SIZE:
					tmpf.write(base64.b64decode(buffer[:BLOCK_SIZE]))
					buffer = buffer[:BLOCK_SIZE]
			tmpf.write(base64.b64decode(buffer))

			tmpf.close()

			# watchdog reports a file-deleted and a file-created event, so ignore both.
			request.client._ignore_next_fsevent(path)
			request.client._ignore_next_fsevent(path)
			os.rename(tmpf.name, request.client._abspath(path))

			request.client._update_metadata(path)
			request.client._event_handler._dispatch(
				'received', request.client, path, 'file'
			)

		elif action == 'send':
			request.queue_file('send', path)

		else:
			log.warn('FileRoute: Unknown action \'{0}\', igoring.', action)

		# If we are the 'server', we also need to distribute all file request
		# to all other clients.
		if not request.is_client:
			action = 'send' if action == 'receive' else 'request'
			request.client._run_on_peers('queue_file', request, action, path)


"""
	{
		"type": "<type>",
		"path": "<relpath-to-file>"
	}
	<type> can be one of 'file-created', 'file-deleted', 'file-moved'

"""
class EventRoute(Route):
	def handle(self, data, request):
		f_type, event = data['type'].split('-')
		path = data['path']
		abspath = request.client._abspath(path)

		request.client._ignore_next_fsevent(path)

		# TODO: factor out common code with Client._handle_fsevent() and Client._merge_filelist()
		if event == 'created':
			# create the file/directory
			if f_type == 'file':
				open(abspath, 'a').close()
			else:
				os.mkdir(abspath, 0o755)

			request.client._add_to_filelist(path, f_type)

		elif event == 'deleted':
			request.client._remove_from_filelist(path)
			os.remove(abspath)

		elif event == 'moved':
			request.client._remove_from_filelist(path)
			os.rename(abspath, data['dest'])
			request.client._add_to_filelist(data['dest'], f_type)

		else:
			log.warn('EventRoute: Unknown event {0}', data)

		# For rationale, see FileRoute.handle()
		if not request.is_client:
			request.client._run_on_peers('queue_event', request, data)

ROUTES = {
	'filelist': FilelistRoute(),
	'file': FileRoute(),
	'event': EventRoute()
}


"""
	behem0th's protocol is completely text-based, using utf-8 encoding and
	encoded in JSON for easy parsing.

	A request usually looks like this:
		{ "route": "<route-name>", "data": "<data>" }

	'data' holds additional data which is then passed to the route.
	There is no special format designed for 'data' and is specific to each route.

	After each request there is a newline to separate them. (think of HTTP)

	If a route needs to transfer additional data (a 'payload'), it has to send them
	in a text-based format, e.g. base-64 encoding for binary data.

	After the payload, if any, there has to be another newline to separate it from
	the next request.
"""
class RequestHandler(threading.Thread):
	req_handler_num = 0

	def __init__(self, **kwargs):
		super().__init__()
		self.daemon = True
		self.sync_queue = queue.Queue()
		self.routes = {}
		self.recvbuf = b''

		RequestHandler.req_handler_num += 1
		self.name = "request-handler-{0}".format(RequestHandler.req_handler_num)
		for key, value in kwargs.items():
			setattr(self, key, value)

		with self.client._rlock:
			self.client._peers.append(self)

		self.sock.setblocking(0)
		self.is_client = bool(self.client._sock)

		for name, route in ROUTES.items():
			route.route_name = name
			route.handler = self
			self.routes[name] = route


	def setup(self):
		log.info('Connected to {0}:{1}', self.address[0], self.address[1])

		# If self.client has a (active) socket, it is a client and
		# thus needs to starts syncing up with the server.
		if self.is_client:
			# Lock the client until the filelist has been sent back by the server.
			self.client._rlock.acquire()
			self.send('filelist', self.client._filelist)


	def close(self):
		self.sync_queue.put({'action': 'exit'})

		try:
			self.sock.shutdown(socket.SHUT_RDWR)
		except OSError:
			pass


	def handle(self, data):
		try:
			data = json.loads(data)
		except ValueError:
			log.error('Received invalid data: {0}', data)
			return

		route = data['route']
		data = data['data']

		log.info_v('Handling {0}, data:\n{1}', route, data)

		if route in self.routes:
			self.routes[route].handle(data, self)
		else:
			log.error("Data received on unknown route '{0}'!", route)


	def send(self, route, data):
		request = json.dumps({'route': route, 'data': data}) + '\n'

		self.sock.sendall(request.encode())


	def recv(self):
		if self.recvbuf:
			# This needs special handling because there could be multiple
			# request in recvbuf. If this is the case, we can only yield the first
			# one and have to leave to others in recvbuf.

			index = self.recvbuf.find(b'\n')

			if index == -1:
				yield self.recvbuf
				self.recvbuf = None
			else:
				yield self.recvbuf[:index]
				self.recvbuf = self.recvbuf[index+1:]
				return

		while 1:
			select.select([self.sock], [], [])

			chunk = self.sock.recv(1024)
			if not len(chunk):
				# If select has signaled the socket is readable, yet .recv()
				# returns zero bytes, the other end probably performed
				# a close() or shutdown() on the socket.
				break

			index = chunk.find(b'\n')

			if index == -1:
				yield chunk
			else:
				yield chunk[:index]
				self.recvbuf = chunk[index+1:]
				break


	def queue_file(self, action, path):
		self.sync_queue.put({
			'action': action + '-file',
			'path': path
		})


	def queue_event(self, event):
		self.sync_queue.put({
			'action': 'send-event',
			'event': event
		})


	def sync_worker(self):
		while 1:
			entry = self.sync_queue.get()
			log.info_v('Processing {0}', entry)

			if entry['action'] == 'exit':
				break

			elif entry['action'] == 'send-file':
				path = entry['path']
				abspath = self.client._abspath(path)

				self.send('file', {
					'path': path,
					'action': 'receive'
				})

				for buf in utils.read_file_seq(abspath, BLOCK_SIZE):
					self.sock.sendall(base64.b64encode(buf))
				self.sock.sendall(b'\n')

				self.client._event_handler._dispatch(
					'sent', self.client, path, 'file'
				)

			elif entry['action'] == 'request-file':
				self.send('file', {
					'path': entry['path'],
					'action': 'send'
				})

			elif entry['action'] == 'send-event':
				self.send('event', entry['event'])

			self.sync_queue.task_done()


	def run(self):
		self.setup()

		utils.create_thread(self.sync_worker,
			name=self.name.replace('request-handler', 'sync-worker'))

		while 1:
			buffer = b''
			for chunk in self.recv():
				buffer += chunk

			if not len(buffer):
				break

			self.handle(buffer.decode())


		log.info('Disconnected from {0}:{1}', self.address[0], self.address[1])
		self.close()



[((186, 20, 186, 33), 'queue.Queue', 'queue.Queue', ({}, {}), '()', False, 'import queue\n'), ((208, 2, 208, 68), 'behem0th.log.info', 'log.info', ({(208, 11, 208, 33): '"""Connected to {0}:{1}"""', (208, 35, 208, 50): 'self.address[0]', (208, 52, 208, 67): 'self.address[1]'}, {}), "('Connected to {0}:{1}', self.address[0], self.address[1])", False, 'from behem0th import utils, log\n'), ((237, 2, 237, 53), 'behem0th.log.info_v', 'log.info_v', ({(237, 13, 237, 39): '"""Handling {0}, data:\n{1}"""', (237, 41, 237, 46): 'route', (237, 48, 237, 52): 'data'}, {}), '("""Handling {0}, data:\n{1}""", route, data)', False, 'from behem0th import utils, log\n'), ((355, 2, 355, 73), 'behem0th.log.info', 'log.info', ({(355, 11, 355, 38): '"""Disconnected from {0}:{1}"""', (355, 40, 355, 55): 'self.address[0]', (355, 57, 355, 72): 'self.address[1]'}, {}), "('Disconnected from {0}:{1}', self.address[0], self.address[1])", False, 'from behem0th import utils, log\n'), ((77, 10, 77, 51), 'tempfile.NamedTemporaryFile', 'tempfile.NamedTemporaryFile', (), '', False, 'import tempfile\n'), ((229, 10, 229, 26), 'json.loads', 'json.loads', ({(229, 21, 229, 25): 'data'}, {}), '(data)', False, 'import json\n'), ((242, 3, 242, 60), 'behem0th.log.error', 'log.error', ({(242, 13, 242, 52): '"""Data received on unknown route \'{0}\'!"""', (242, 54, 242, 59): 'route'}, {}), '("Data received on unknown route \'{0}\'!", route)', False, 'from behem0th import utils, log\n'), ((246, 12, 246, 54), 'json.dumps', 'json.dumps', ({(246, 23, 246, 53): "{'route': route, 'data': data}"}, {}), "({'route': route, 'data': data})", False, 'import json\n'), ((268, 3, 268, 37), 'select.select', 'select.select', ({(268, 17, 268, 28): '[self.sock]', (268, 30, 268, 32): '[]', (268, 34, 268, 36): '[]'}, {}), '([self.sock], [], [])', False, 'import select\n'), ((304, 3, 304, 38), 'behem0th.log.info_v', 'log.info_v', ({(304, 14, 304, 30): '"""Processing {0}"""', (304, 32, 304, 37): 'entry'}, {}), "('Processing {0}', entry)", False, 'from behem0th import utils, log\n'), ((86, 14, 86, 38), 'base64.b64decode', 'base64.b64decode', ({(86, 31, 86, 37): 'buffer'}, {}), '(buffer)', False, 'import base64\n'), ((104, 3, 104, 66), 'behem0th.log.warn', 'log.warn', ({(104, 12, 104, 57): '"""FileRoute: Unknown action \'{0}\', igoring."""', (104, 59, 104, 65): 'action'}, {}), '("FileRoute: Unknown action \'{0}\', igoring.", action)', False, 'from behem0th import utils, log\n'), ((135, 4, 135, 28), 'os.mkdir', 'os.mkdir', ({(135, 13, 135, 20): 'abspath', (135, 22, 135, 27): '(493)'}, {}), '(abspath, 493)', False, 'import os\n'), ((141, 3, 141, 21), 'os.remove', 'os.remove', ({(141, 13, 141, 20): 'abspath'}, {}), '(abspath)', False, 'import os\n'), ((231, 3, 231, 48), 'behem0th.log.error', 'log.error', ({(231, 13, 231, 41): '"""Received invalid data: {0}"""', (231, 43, 231, 47): 'data'}, {}), "('Received invalid data: {0}', data)", False, 'from behem0th import utils, log\n'), ((145, 3, 145, 35), 'os.rename', 'os.rename', ({(145, 13, 145, 20): 'abspath', (145, 22, 145, 34): "data['dest']"}, {}), "(abspath, data['dest'])", False, 'import os\n'), ((149, 3, 149, 50), 'behem0th.log.warn', 'log.warn', ({(149, 12, 149, 43): '"""EventRoute: Unknown event {0}"""', (149, 45, 149, 49): 'data'}, {}), "('EventRoute: Unknown event {0}', data)", False, 'from behem0th import utils, log\n'), ((318, 15, 318, 55), 'behem0th.utils.read_file_seq', 'utils.read_file_seq', ({(318, 35, 318, 42): 'abspath', (318, 44, 318, 54): 'BLOCK_SIZE'}, {}), '(abspath, BLOCK_SIZE)', False, 'from behem0th import utils, log\n'), ((84, 16, 84, 53), 'base64.b64decode', 'base64.b64decode', ({(84, 33, 84, 52): 'buffer[:BLOCK_SIZE]'}, {}), '(buffer[:BLOCK_SIZE])', False, 'import base64\n'), ((319, 23, 319, 44), 'base64.b64encode', 'base64.b64encode', ({(319, 40, 319, 43): 'buf'}, {}), '(buf)', False, 'import base64\n')]