#!/usr/bin/python3

'This is the server part of openmsx-sync. See README.md for details.'

import fhs
import websocketd
import os
import sys
import base64
import traceback

# Parse options and prepare machinery for using system files.
fhs.option('port', 'server port', default = '4567')
config = fhs.init(game = True)

# Find directory with region definitions per game.
# This searches in the current directory and in standard xdg paths and returns a list of all matches.
gamesdirs = fhs.read_data('games', dir = True, multiple = True)

def read_regions(gamesdir, name):
	'''Read regions definition for a single game.
	name is the filename for the regions file, in the games directory (without the extension).
	Returns a dict with int adresses as keys and the initial data (all zero) as values.
	Errors are not handled here; they raise an exception which causes the game to be ignored.'''
	data = open(os.path.join(gamesdir, name + '.regions')).read().split('\n')
	rom = open(os.path.join(gamesdir, data.pop(0)), 'rb').read()
	ret = {}
	for region in data:
		if len(region.strip()) == 0 or region.strip().startswith('#'):
			continue
		r = region.split()
		# Larger than 2 is not a problem; there may be comments after the region definition.
		if len(r) < 2:
			continue
		start = int(r[0], 16)
		end = int(r[1], 16)
		# Use (hex) str as key, because json converts keys to str anyway.
		ret['%04x' % start] = [0] * (end - start + 1)
	return ret, rom

# Find all supported games and initialize their shared data.
games = {}
roms = {}
for gamesdir in gamesdirs:
	for name in os.listdir(gamesdir):
		base, ext = os.path.splitext(name)
		if base.startswith('.') or ext != os.extsep + 'regions':
			continue
		if base in games:
			print('ignoring duplicate definition of %s' % base)
			continue
		try:
			games[base], roms[base] = read_regions(gamesdir, base)
		except FileNotFoundError:
			print('ignoring game %s, because the rom file cannot be found' % base, file = sys.stderr)
		except:
			print('ignoring game %s, because there was an error reading the regions file' % base, file = sys.stderr)
			traceback.print_exc()

class Connection:
	'''An instance of this class is created when a new websocket connection is established.
	Its only attribute is gamename, the game that is syncing through this connection.
	Member functions (that don't start with underscore) can be called by the remote end.
	'''
	def __init__(self, remote):
		'Send a list of games. In response the remote end should select a game.'
		gamename = None
		remote.games.event(tuple(games.keys()))
	def game(self, name):
		'Remote end calls this to select a game to sync. Returns synced regions, including their current contents.'
		assert name in games
		self.gamename = name
		return games[name], base64.b64encode(roms[name]).decode('utf-8')
	def notify(self, region, data):
		'''Remote end calls this to report changed data. The entire region is always sent.
		This function returns None, but also broadcasts the changed data to everyone (including the caller).'''
		assert region in games[self.gamename]
		assert all(isinstance(x, int) for x in data)
		assert len(games[self.gamename][region]) == len(data)
		games[self.gamename][region] = data
		server.broadcast.update(self.gamename, region, data)

# Start server and wait for events.
server = websocketd.RPChttpd(config['port'], Connection)
print('server is running on port %s' % config['port'], file = sys.stderr)
websocketd.fgloop()
