#!/usr/bin/env python
# vim: set encoding=utf-8
# wherpygo - player for wherigo cartridges.
# Copyright 2012 Bas Wijnen <wijnen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Color definitions.
# Stuff on the map.
zonescolor = '#00f'	# also in a tab.
characterscolor = '#f00'
objectscolor =  '#880'
positioncolor = '#000'
gridcolor = '#ccc'

# Stuff in the tabs.
environmentcolor = '#f00'
inventorycolor = '#880'
taskcolor = '#000'
messagecolor = '#000'
historycolor = '#888'
logcolor = '#080'
timercolor = '#f0f'

import sys
sys.path += ['/usr/share/wherpygo']
import lua
import re
import gps
import gtk
import gwc
import time
import wherigo
import Map
import gobject
gobject.threads_init ()
try:
	import gst
except:
	pass
import argparse

a = argparse.ArgumentParser ()
a.add_argument ('cartridge', default = None, nargs = '?', help = 'The cartridge to load', type = str)
a.add_argument ('--debug', help = 'Enable debugging mode', default = False, action = 'store_true')
args = a.parse_args ()
gwcfile = args.cartridge
debug = args.debug
class Settings:
	def __init__ (self):
		self.show_start = False
settings = Settings ()

def backtrace ():
	sys.stderr.write ('=' * 78 + '\n')
	try:
		d = 0
		while True:
			frame = sys._getframe (d)
			sys.stderr.write ('\t%s:%d %s\n' % (frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name))
			d += 1
	except ValueError:
		pass
	sys.stderr.write ('=' * 78 + '\n')

minloglevel = wherigo.LOGDEBUG

class Position:
	def __init__ (self, lat = None, lon = None, alt = None, speed = None, climb = None, track = None, time = None):
		self.lat = lat
		self.lon = lon
		self.alt = alt
		self.speed = speed
		self.climb = climb
		self.track = track
		self.time = time
	def __nonzero__ (self):
		return self.time is not None
	def __str__ (self):
		if self.time == None:
			return 'None'
		return 'lat:%f lon:%f alt:%f speed:%f climb:%f track:%f time:%s' % (self.lat, self.lon, self.alt, self.speed, self.climb, self.track, self.time)

class GpsPoller:
	def __init__(self):
		self.current_value = Position ()
		self.idle = [10, None]
		self.session = None
	def get_pos (self):
		if self.session is None:
			try:
				self.session = gps.gps (mode = gps.WATCH_ENABLE)
			except:
				self.session = None
				return Position ()
		try:
			while self.session.waiting ():
				value = self.session.next ()
				# Save only position, not every event.
				if value['class'] == 'DEVICE' and value['activated'] == 0:
					self.current_value = Position ()
					continue
				if value['class'] != 'TPV':
					continue
				if value['mode'] == 1 or 'lat' not in value or 'lon' not in value:
					self.current_value = Position ()
				else:
					self.current_value = Position (value['lat'], value['lon'], value['alt'] if 'alt' in value else 0, value['speed'] if 'speed' in value else 0, value['climb'] if 'climb' in value else 0, value['track'] if 'track' in value else 0, value['time'] if 'time' in value else 0)
		except:
			self.current_value = Position ()
		#print self.current_value
		ret = self.current_value
		if self.idle[1] == ret.time:
			if self.idle[0] == 10:
				self.idle = [0, None]
				ret = Position ()
				self.current_value = ret
				self.session = None
			else:
				self.idle[0] += 1
		else:
			self.idle = [0, ret.time]
		return ret

def update_title (screen):
	t = screen.tabname
	if screen.show_size:
		t += ' (%d)' % screen.size
	t = '<span foreground="%s">%s</span>' % (screen.color, t)
	if screen.new:
		t = '<b>' + t + '</b>'
	screen.title.set_markup (t)

def make_str (degs):
	'Make a string value from a float degree value'
	deg = int (degs)
	degs -= deg
	degs *= 60
	min = int (degs)
	degs -= min
	degs *= 60
	sec = degs
	return '''%d°%d'%.2f"''' % (deg, min, sec)

class Detail (gtk.VBox):
	def __init__ (self):
		gtk.VBox.__init__ (self)
		self.buttons = gtk.HBox ()
		self.pack_start (self.buttons, False)
		self.text = gtk.TextView ()
		self.text.set_can_focus (False)
		self.text.set_wrap_mode (gtk.WRAP_WORD)
		self.text.set_editable (False)
		self.pack_end (self.text, True)
		self.image = gtk.Image ()
		self.pack_end (self.image, False)
	def set (self, media, text, buttons, cb):
		# Command argument protocol:
		# Per command, there is one line.
		#	[...]
		# For a no-target command, the line shows the name of the command as a single button.
		#	(str, str): button title, command name.
		# For a command with target, the line starts with a label containing the name of the command.
		# If there is no available target, this is followed by a label with the EmptyTargetListText.
		#	((str, str), str): (first label, second label), command name
		# If there are targets, this is followed by a list of buttons (name, object), one per target.
		#	(str, str, [(str, ZObject), (str, ZObject), ...]): first label, command name, (button label, item) (first label can be None)
		if media:
			if media.Id in gameobject._image:
				self.image.set_from_pixbuf (gameobject._image[media.Id])
		else:
			self.image.clear ()
		self.text.get_buffer ().set_text (re.sub ('\s*<br>\s*', '\n', re.sub ('\s+', ' ', text.replace ('&amp;', '&').replace ('&nbsp;', ' '), 0, re.DOTALL), 0, re.IGNORECASE | re.DOTALL))
		self.remove (self.buttons)
		self.buttons = gtk.VBox ()
		self.pack_start (self.buttons, False)
		for b in buttons:
			if len (b) == 1:
				# This is a text entry.
				entry = gtk.Entry ()
				self.buttons.pack_start (entry, True)
				if cb is not None:
					entry.connect ('activate', cb, b[0])
					entry.grab_focus ()
			elif len (b) == 2 and isinstance (b[0], str):
				# This is a single button.
				button = gtk.Button (b[0])
				if cb is not None:
					button.connect ('clicked', cb, b[1])
				button.set_can_focus (True)
				self.buttons.pack_start (button, True)
			elif len (b) == 2:
				# This is a Works-with command without a target.
				box = gtk.HBox ()
				self.buttons.pack_start (box, True)
				box.pack_start (gtk.Label (b[0][0]), False)
				box.pack_start (gtk.Label (b[0][1]), True)
			else:
				# This is a Works-with command with targets.
				box = gtk.HBox ()
				self.buttons.pack_start (box, True)
				if b[0] is not None:
					box.pack_start (gtk.Label (b[0]), False)
				for t in b[2]:
					button = gtk.Button (t[0])
					button.set_can_focus (True)
					if cb is not None:
						button.connect ('clicked', cb, b[1], t[1])
					box.pack_start (button, True)
		self.buttons.show_all ()

class List (gtk.ScrolledWindow):
	def __init__ (self, title, with_icon = False):
		gtk.ScrolledWindow.__init__ (self)
		self.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
		self.box = gtk.VBox ()
		self.add_with_viewport (self.box)
		if with_icon:
			self.with_icon = True
			self.store = gtk.ListStore (str, str, object, bool, bool, str)
		else:
			self.with_icon = False
			self.store = gtk.ListStore (str, str, object, bool, bool)
		self.treeview = gtk.TreeView (self.store)
		self.treeview.set_can_focus (False)
		if debug:
			self.activecolumn = gtk.TreeViewColumn ('active')
			self.visiblecolumn = gtk.TreeViewColumn ('visible')
			self.treeview.append_column (self.activecolumn)
			self.treeview.append_column (self.visiblecolumn)
			self.activerenderer = gtk.CellRendererToggle ()
			self.visiblerenderer = gtk.CellRendererToggle ()
			self.activecolumn.pack_start (self.activerenderer, True)
			self.visiblecolumn.pack_start (self.visiblerenderer, True)
			self.activecolumn.add_attribute (self.activerenderer, 'active', 3)
			self.visiblecolumn.add_attribute (self.visiblerenderer, 'active', 4)
		if with_icon:
			self.iconcolumn = gtk.TreeViewColumn ('State')
			self.treeview.append_column (self.iconcolumn)
			self.iconrenderer = gtk.CellRendererPixbuf ()
			self.iconcolumn.pack_start (self.iconrenderer, True)
			self.iconcolumn.add_attribute (self.iconrenderer, 'stock-id', 5)
		self.column = gtk.TreeViewColumn (title)
		self.treeview.append_column (self.column)
		self.renderer = gtk.CellRendererText ()
		self.column.pack_start (self.renderer, True)
		self.column.add_attribute (self.renderer, 'text', 0)
		self.column.add_attribute (self.renderer, 'foreground', 1)
		self.treeview.set_reorderable (True)
		self.treeview.get_selection ().connect ('changed', self.selection_changed)
		self.box.pack_start (self.treeview, False)
		self.box.pack_start (gtk.HSeparator (), False)
		self.details = Detail ()
		self.box.pack_start (self.details, True)
		self.selected_item = None
	def _find (self, model, path, iter, data):
		if model.get_value (iter, 0) == data[0]:
			data[1] = iter
			return True
	def select (self, item):
		data = [item, None]
		self.store.foreach (self._find, data)
		if data[1] == None:
			return
		self.treeview.get_selection ().select_iter (data[1])
	def selection_changed (self, selection):
		# Update details according to new selection.
		if not gameobject:
			return
		i = selection.get_selected ()[1]
		buttons = []
		if i:
			self.selected_item = self.store.get_value (i, 2)
			media = self.selected_item.Media
			text = self.selected_item.Description
			if not self.selected_item.Locked: # No commands for locked items.
				for c in self.selected_item.Commands:
					cmd = self.selected_item.Commands[c]
					if not cmd.Enabled:
						continue
					t = cmd.Text
					if cmd.CmdWith:
						l = []
						for k in gameobject.AllZObjects if cmd.WorksWithAll else cmd.WorksWithList._list ():
							if k is not self.selected_item and k._is_visible (debug):
								l.append ((k.Name, k))
						if len (l) == 0:
							buttons.append (((t, cmd.EmptyTargetListText), c))
						else:
							buttons.append ((t, c, l))
					else:
						buttons.append ((t, c))
		else:
			self.selected_item = None
			media = None
			text = ''
		self.details.set (media, text, buttons, self._button)
		# Update markers on map, if any.
		self._update_map ()
	def _button (self, widget, name, item = None):
		attr = 'On' + name
		cmd = self.selected_item.Commands[name]
		if hasattr (self.selected_item, attr):
			getattr (self.selected_item, attr) (self.selected_item, item)
			cbs.update ()
		else:
			print ('Command "%s" not running, because no callback was registered.' % name)
		return True
	def _update_map (self):
		# Overloaded from Lists which need it.
		# Update some info here as well.
		self.update_stats ()
	def update_stats (self):
		pass
	def update (self):
		keys, full = self.makelist ()
		self.size = 0
		# Step 1: remove all items that should not be present.
		current = self.store.get_iter_first ()
		while current:
			next = self.store.iter_next (current)
			k = self.store.get_value (current, 0)
			c = keys.count (k)
			pos = 0
			while c > 0:
				i = keys.index (k, pos)
				v = self.store.get_value (current, 2)
				if full[i][2] is v:
					self.size += 1
					if self.with_icon:
						self.store.set_value (current, 5, gtk.STOCK_EXECUTE if not v.Complete else gtk.STOCK_APPLY if v.CorrectState.lower () not in ('incorrect', 'notcorrect') else gtk.STOCK_CANCEL)
					keys.pop (i)
					full.pop (i)
					break
				else:
					print 'other thing with same name: (%s, %s)' % (full[i], v)
				pos = i + 1
				c -= 1
			else:
				self._remove_item (current)
				self.new = True
			current = next
		# Step 2: add all missing items
		for f in full:
			self.size += 1
			self.new = True
			self._add (f)
		# Step 3: update the information
		if debug:
			current = self.store.get_iter_first ()
			while current:
				i = self.store.get_value (current, 2)
				self.store.set_value (current, 3, i.Active != 0)
				self.store.set_value (current, 4, i.Visible != 0)
				self._debug_update_map (i)
				current = self.store.iter_next (current)
		self.selection_changed (self.treeview.get_selection ())
		map.update ()
		self.update_stats ()
		update_title (self)
	def _add (self, data):
		# Overload this to do more.
		self.store.append (data)
	def _remove_item (self, item):
		self.store.remove (item)
	def _debug_update_map (self, i):
		pass

class MarkerList (List):
	'''A list with links to markers on the map'''
	def __init__ (self, map, desc, layers):
		List.__init__ (self, desc)
		self.map = map
		self.layers = layers
		self.selected = None
		hbox = gtk.HBox ()
		hbox.pack_start (gtk.Label ('Selected:'), False)
		self.lat = gtk.Label ()
		hbox.pack_start (self.lat, True)
		self.lon = gtk.Label ()
		hbox.pack_start (self.lon, True)
		self.alt = gtk.Label ()
		hbox.pack_start (self.alt, True)
		self.box.pack_start (hbox, False)
		hbox = gtk.HBox ()
		hbox.pack_start (gtk.Label ('To selected:'), False)
		self.bearing = gtk.Label ()
		hbox.pack_start (self.bearing, True)
		self.distance = gtk.Label ()
		hbox.pack_start (self.distance, True)
		self.box.pack_start (hbox, False)
		self.treeview.connect ('row-activated', self._activate)
	def _activate (self, widget, path, column):
		i = self.store.get_iter (path)
		obj = self.store.get_value (i, 2)
		# TODO: trigger OnClicked?
		pos = self._get_pos (obj)
		self.map.set_pos ((pos.latitude, pos.longitude))
	def _add (self, data):
		self.store.append (data)
		summary, color, info, active, visible = data
		pos = self._get_pos (info)
		if pos:
			layer = self._get_layer (info)
			info._id = len (self.layers[layer].markers)
			self.layers[layer].markers += ([(pos.latitude, pos.longitude), [False, active and visible], info],)
			self.map.update ()
		else:
			info._id = None
	def _get_pos (self, info):
		while hasattr (info, 'Container') and info.Container is not None:
			info = info.Container
		if isinstance (info, wherigo.Zone):
			return info.OriginalPoint
		else:
			return info.ObjectLocation
	def update_stats (self):
		if self.selected_item:
			if self.selected_item.CurrentDistance.GetValue ('meters') < 1000:
				diststr = '%d m' % self.selected_item.CurrentDistance.GetValue ('meters')
			else:
				diststr = '%.2f km' % self.selected_item.CurrentDistance.GetValue ('kilometers')
			self.distance.set_text (diststr)
			self.bearing.set_text ('%d°' % self.selected_item.CurrentBearing.value)
			self.lat.set_text (make_str (self._get_pos (self.selected_item).latitude))
			self.lon.set_text (make_str (self._get_pos (self.selected_item).longitude))
			self.alt.set_text ('%d m' % self._get_pos (self.selected_item).altitude)
		else:
			self.distance.set_text ('-')
			self.bearing.set_text ('-')
			self.lat.set_text ('-')
			self.lon.set_text ('-')
			self.alt.set_text ('-')
	def _get_layer (self, info):
		# Overloadable for multi-layer lists.
		return 0
	def _update_map (self):
		# Refresh selected status of markers on the map.
		if self.selected is not None and self.selected._id is not None:
			layer = self._get_layer (self.selected)
			self.layers[layer].markers[self.selected._id][1][0] = False
		if self.selected_item is not None and self.selected_item._id is not None:
			layer = self._get_layer (self.selected_item)
			self.layers[layer].markers[self.selected_item._id][1][0] = True
		self.selected = self.selected_item
		self.map.update ()
		List._update_map (self)
	def _debug_update_map (self, item):
		'Update active and visible status on map'
		if item._id is not None:
			layer = self._get_layer (item)
			self.layers[layer].markers[item._id][1][1] = item.Active and item.Visible
	def _remove_item (self, item):
		i = self.store.get_value (item, 2)
		List._remove_item (self, item)
		if i._id is not None:
			layer = self._get_layer (i)
			del self.layers[layer].markers[i._id]
			for check in self.layers[layer].markers:
				if check[2]._id >= i._id:
					check[2]._id -= 1
	def update_map (self):
		# Refresh all marker coordinates on the map.
		for layer in self.layers:
			for marker in layer.markers:
				marker[0] = (marker[2].ObjectLocation.latitude, marker[2].ObjectLocation.longitude)

class Locations (MarkerList):
	# Compass and list of locations; selected location is shown on compass.
	def __init__ (self, layer):
		MarkerList.__init__ (self, layer.map, 'Location', (layer,))
	def makelist (self):
		if not gameobject:
			return [], []
		kret = []
		fret = []
		for i in gameobject.AllZObjects:
			if isinstance (i, wherigo.Zone):
				if debug or (i.Active and i.Visible):
					kret += (i.Name,)
					fret += ((i.Name, zonescolor, i, i.Active, i.Visible),)
		return kret, fret
	def _update_map (self):
		# Add zone boundary for selected zone.
		if self.selected_item is None or self.selected_item._id is None:
			self.layers[0].tracks = []
		else:
			track = [(x.latitude, x.longitude) for x in self.selected_item.Points._list ()]
			track += (track[0],)
			self.layers[0].tracks = [track]
		# Do all the usual stuff.
		MarkerList._update_map (self)

class Inventory (List):
	def __init__ (self):
		List.__init__ (self, 'Item')
	def makelist (self):
		if not gameobject:
			return [], []
		kret = []
		fret = []
		for i in gameobject.AllZObjects:
			if i._is_visible (debug) and i.Container is wherigo.Player:
				kret += (i.Name,)
				fret += ((i.Name, inventorycolor, i, i.Active, i.Visible),)
		return kret, fret

class Environment (MarkerList):
	def __init__ (self, objlayer, charlayer):
		MarkerList.__init__ (self, objlayer.map, 'Item or person', (objlayer, charlayer))
	def _get_layer (self, info):
		return 1 if isinstance (info, wherigo.ZCharacter) else 0
	def makelist (self):
		if not gameobject:
			return [], []
		if settings.show_start:
			kret = [wherigo._starting_marker.Name]
			fret = [(wherigo._starting_marker.Name, positioncolor, wherigo._starting_marker, True, True)]
		else:
			kret = []
			fret = []
		for i in gameobject.AllZObjects:
			if i._is_visible (debug) and i.Container is not wherigo.Player:
				kret += (i.Name,)
				fret += ((i.Name, characterscolor if isinstance (i, wherigo.ZCharacter) else objectscolor, i, i.Active, i.Visible),)
		return kret, fret

class Tasks (List):
	def __init__ (self):
		List.__init__ (self, 'Task', True)
	def makelist (self):
		if not gameobject:
			return [], []
		kret = []
		fret = []
		for i in gameobject.AllZObjects:
			if isinstance (i, wherigo.ZTask) and (debug or (i.Active and i.Visible)):
				kret += (i.Name,)
				fret += ((i.Name, taskcolor, i, i.Active, i.Visible, gtk.STOCK_EXECUTE if not i.Complete else gtk.STOCK_APPLY if i.CorrectState.lower () not in ('incorrect', 'notcorrect') else gtk.STOCK_CANCEL),)
		return kret, fret

class History (gtk.VBox):
	def __init__ (self):
		gtk.VBox.__init__ (self)
		self.store = gtk.ListStore (str, object)
		self.selector = gtk.ComboBox (self.store)
		cell = gtk.CellRendererText ()
		self.selector.pack_start (cell)
		self.selector.add_attribute (cell, 'text', 0)
		self.pack_start (self.selector, False)
		self.detail = Detail ()
		self.pack_start (self.detail, True)
		self.selector.connect ('changed', self.update)
		self.size = 0
	def add_item (self, media, text, buttons):
		title = time.strftime ('%X')
		self.store.append ((title, (media, text, buttons, None)))
		self.size += 1
		update_title (self)
	def update (self, widget):
		i = self.selector.get_active_iter ()
		if i is None:
			self.detail.set (media = None, text = '', buttons = (), cb = None)
		else:
			item = self.store.get_value (i, 1)
			self.detail.set (*item)

class Log (gtk.TreeView):
	def __init__ (self):
		self.store = gtk.ListStore (str, str, str, str)
		gtk.TreeView.__init__ (self, self.store)
		self.columns = [gtk.TreeViewColumn (x) for x in ('Time', 'Level', 'Message')]
		self.renderer = [gtk.CellRendererText () for x in range (len (self.columns))]
		for c in range (len (self.columns)):
			self.append_column (self.columns[c])
			self.columns[c].pack_start (self.renderer[c])
			self.columns[c].add_attribute (self.renderer[c], 'text', c)
			self.columns[c].add_attribute (self.renderer[c], 'foreground', 3)
	def add_log (self, level, levelname, text):
		t = time.strftime ('%X')
		self.store.append ((t, levelname, text, logcolor))
		self.size += 1
		self.new = True
		update_title (self)

class Timers (gtk.TreeView):
	def __init__ (self):
		signature = (str, str, bool, str, str, str, object)
		self.store = gtk.ListStore (*signature)
		gtk.TreeView.__init__ (self, self.store)
		self.set_can_focus (False)
		self.columns = [gtk.TreeViewColumn (x) for x in ('Name', 'Type', 'Running', 'Remaining', 'Duration')]
		self.renderer = [gtk.CellRendererToggle () if signature[c] == bool else gtk.CellRendererText () for c in range (len (self.columns))]
		for c in range (len (self.columns)):
			self.append_column (self.columns[c])
			self.columns[c].pack_start (self.renderer[c], True)
			self.columns[c].add_attribute (self.renderer[c], 'active' if signature[c] is bool else 'text', c)
			if signature[c] is not bool:
				self.columns[c].add_attribute (self.renderer[c], 'foreground', 5)
		self.set_reorderable (True)
	def add_timer (self, name):
		self.store.append ((name, 'Countdown', False, '', ''))
	def mktime (self, t):
		return '%5.2f' % t
	def update (self):
		if not gameobject:
			return [], []
		keys = []
		full = []
		for i in gameobject.AllZObjects:
			if isinstance (i, wherigo.ZTimer):
				keys += (i.Name,)
				full += ((i.Name, '', False, '', '', timercolor, i),) # Actual values are filled in below, in step 3.
		# Step 1: remove all items that should not be present.
		current = self.store.get_iter_first ()
		while current:
			next = self.store.iter_next (current)
			k = self.store.get_value (current, 0)
			if k not in keys:
				self.store.remove (current)
			else:
				i = keys.index (k)
				keys.pop (i)
				full.pop (i)
			current = next
		# Step 2: add all missing items
		for f in full:
			self.store.append (f)
		# Step 3: update contents.
		current = self.store.get_iter_first ()
		while current:
			i = self.store.get_value (current, 6)
			self.store.set_value (current, 1, i.Type)
			self.store.set_value (current, 2, i._target is not None)
			self.store.set_value (current, 3, int (i.Remaining))
			self.store.set_value (current, 4, '%6.3f' % i.Duration)
			current = self.store.iter_next (current)

class CB:
	pipeline = None
	def dialog (self, table):
		global queue
		self.update ()
		queue_reset ()
		for m in table._list ():
			queue += (m,)
		next_message ()
		self.update ()
	def message (self, table):
		global queue
		self.update ()
		queue_reset ()
		queue += (table,)
		next_message ()
		self.update ()
	def get_input (self, zinput):
		global queue
		self.update ()
		queue_reset ()
		queue += (zinput,)
		next_message ()
		self.update ()
	def play (self, media):
		self.stop_sound ()	# includes update.
		if media.Id not in gameobject._sound:
			return
		try:
			CB.pipeline = gst.Pipeline ("pipeline")
			CB.source = gst.element_factory_make ("appsrc", "source")
			CB.pipeline.add (CB.source)
			CB.decoder = gst.element_factory_make ("decodebin", "decoder")
			CB.pipeline.add (CB.decoder)
			CB.converter = gst.element_factory_make ("audioconvert", "converter")
			CB.pipeline.add (CB.converter)
			CB.sink = gst.element_factory_make ("alsasink", "sink")
			CB.pipeline.add (CB.sink)
			CB.source.link (CB.decoder)
			CB.decoder.connect ('new-decoded-pad', self.new_pad)
			CB.converter.link (CB.sink)
			CB.pipeline.set_state (gst.STATE_PAUSED)
			CB.source.emit ('push-buffer', gst.Buffer (gameobject._sound[media.Id]))
		except:
			pass
	def new_pad (self, dbin, pad, is_last):
		try:
			CB.decoder.link (CB.converter)
			CB.pipeline.set_state (gst.STATE_PLAYING)
		except:
			pass
	def stop_sound (self):
		self.update ()
		try:
			if self.pipeline:
				self.pipeline.set_state (gst.STATE_NULL)
				self.pipeline = None
				self.source = None
				self.decoder = None
				self.converter = None
				self.sink = None
		except:
			pass
	def set_status (self, text):
		self.update ()
		statusbar.pop (statuscontext)
		statusbar.push (statuscontext, text)
	def save (self):
		if gameobject.OnSync:
			gameobject.OnSync ()
		self.update ()
		save_cb (None, 1)
	def quit (self):
		self.update ()
		print ('request to quit')
		#gtk.main_quit ()
	def drive_to (self, *a):
		self.update ()
		print ("I'm supposed to drive to", a)
		# which is?
		# TODO
	def alert (self):
		self.update ()
		print 'alert!'
		# Note: this doesn't do anything (on machines without a pc speaker?)
		gtk.gdk.beep ()
	def log (self, level, levelname, text):
		self.update ()
		if level >= minloglevel:
			#print 'Log %s: %s' % (levelname, text)
			log.add_log (level, levelname, text)
	def show (self, screen, item):
		# 'Detail', 'Inventory', 'Item', 'Location', 'Main', 'Tasks'
		self.update ()
		if screen == wherigo.DETAILSCREEN:
			if isinstance (item, wherigo.ZCharacter):
				notebook.set_current_page (notebook.page_num (environmentscreen))
				environmentscreen.select (item)
			elif isinstance (item, wherigo.Zone):
				notebook.set_current_page (notebook.page_num (locationscreen))
				locationscreen.select (item)
			elif isinstance (item, wherigo.ZItem):
				if item.Container == wherigo.Player:
					notebook.set_current_page (notebook.page_num (inventoryscreen))
					inventoryscreen.select (item)
				else:
					notebook.set_current_page (notebook.page_num (environmentscreen))
					environmentscreen.select (item)
			elif isinstance (item, wherigo.ZTask):
				notebook.set_current_page (notebook.page_num (taskscreen))
				taskscreen.select (item)
			else:
				raise AssertionError ('Invalid type to show details for')
		elif screen == wherigo.INVENTORYSCREEN:
			notebook.set_current_page (notebook.page_num (inventoryscreen))
		elif screen == wherigo.ITEMSCREEN:
			notebook.set_current_page (notebook.page_num (environmentscreen))
		elif screen == wherigo.LOCATIONSCREEN:
			notebook.set_current_page (notebook.page_num (locationscreen))
		elif screen == wherigo.MAINSCREEN:
			pass
		elif screen == wherigo.TASKSCREEN:
			notebook.set_current_page (notebook.page_num (taskscreen))
		else:
			raise AssertionError ('Invalid screen to show')
	def update (self):
		locationscreen.update ()
		inventoryscreen.update ()
		environmentscreen.update ()
		taskscreen.update ()
		if debug:
			timers.update ()
	def update_stats (self):
		locationscreen.update_stats ()
		environmentscreen.update_stats ()
	def update_map (self):
		locationscreen.update_map ()
		environmentscreen.update_map ()

def next_message (widget = None, name = None, item = None):
	global queue, current_msg, last_screen
	# current_msg is the current message object
	m = current_msg
	if len (queue) != 0:
		current_msg = queue.pop (0)
	else:
		current_msg = None
	next_current = current_msg
	if m is not None:
		# Fire event for previous message
		if isinstance (m, wherigo.ZInput):
			# No arguments.
			assert name is None
			if hasattr (m, 'OnGetInput'):
				if m.InputType == 'MultipleChoice':
					assert item is not None
					m.OnGetInput (m, item)
				elif m.InputType == 'Text':
					assert item is None
					m.OnGetInput (m, widget.get_text ())
					cbs.update ()
				else:
					raise AssertionError ('unknown input type')
			else:
				print 'no callback for input'

		else:
			# Argument is passed to button callback.
			if 'Callback' in m:
				# for Ok buttons, name is used; for choices, item is used. Allow both.
				m['Callback'] (name if item is None else item)
				cbs.update ()
			else:
				#print 'not running Callback for %s' % m['Name']
				pass
	if current_msg is None:
		notebook.set_current_page (last_screen)
		message.hide ()
		return
	if next_current is not current_msg:
		# The queue changed since this was set; don't interfere.
		return
	if isinstance (current_msg, wherigo.ZInput):
		if hasattr (current_msg, 'Media'):
			media = current_msg.Media
		else:
			media = None
		text = current_msg.Text
		if current_msg.InputType == 'MultipleChoice':
			buttons = [(None, None, [(x, x) for x in current_msg.Choices._list ()])]
		elif current_msg.InputType == 'Text':
			buttons = [(None,)]
		else:
			raise AssertionError ('unknown input type')
	else:
		if 'Media' in current_msg:
			media = current_msg['Media']
		else:
			media = None
		text = current_msg['Text']
		if 'Buttons' in current_msg:
			b = current_msg['Buttons']._list ()
			buttons = [(None, None, [(b[t], 'Button%d' % (t + 1)) for t in range (len (b))])]
		else:
			buttons = [('Ok', 'Button1')]
	message.set (media, text, buttons, next_message)
	history.add_item (media, text, buttons)
	message.show ()
	p = notebook.get_current_page ()
	if p != notebook.page_num (message):
		last_screen = p
	notebook.set_current_page (notebook.page_num (message))

def queue_reset ():
	global queue
	queue = []

# GUI actions.
def file_new (widget):
	global gameobject
	gameobject = None
	wherigo.ZCartridge ()._new ()
	message.hide ()
	cbs.update ()

def file_open (widget):
	opendialog.show ()

def file_save (widget):
	savedialog.show ()

def file_quit (widget):
	gtk.main_quit ()

def help_about (widget):
	aboutdialog.show ()

def open_cb (widget, response):
	if response != 0:
		open_cartridge (widget.get_filename ())
	opendialog.hide ()
	return True

def save_cb (widget, response):
	if response != 0:
		# TODO: not implemented yet.
		pass
	savedialog.hide ()
	return True

def open_cartridge (cartfile):
	global gameobject

	def start_game (widget = None, name = None):
		locationscreen.show ()
		inventoryscreen.show ()
		environmentscreen.show ()
		taskscreen.show ()
		history.show ()
		log.show ()
		notebook.set_current_page (last_screen)
		message.hide ()
		settings.show_start = False
		if gameobject.OnStart:
			gameobject.OnStart ()
		cbs.update ()

	file_new (None)
	script = lua.lua ()
	script.module ('Wherigo', wherigo)
	game = gwc.cartridge (cartfile)
	script.run ('', 'Env', {
		'Platform': 'Python',
		'CartFolder': '/whatever',
		'SyncFolder': '/whatever',
		'PathSep': '/',
		'DeviceID': 'python-id',
		'Version': '2.11-compatible',
		'CartFilename': cartfile,
		'LogFolder': '/whatever',
		'Downloaded': 0,
		'Device': game.device}, name = 'setting Env')
	cartridge = wherigo.ZCartridge ()
	gameobject = cartridge._setup (game, cbs, script)
	settings.show_start = True
	text = 'You are about to play "%s":<br><br>' % gameobject.Name +  gameobject.Description + '<br><br>Please go to the starting point and press ok.<br>The starting point for this cartridge is defined as follows:<br><br>' +  gameobject.StartingLocationDescription
	buttons = [('Ok', 'Ok')]
	history.add_item ( gameobject.Media, text, buttons)
	message.set (gameobject.Media, text, buttons, start_game)
	message.show ()
	notebook.set_current_page (notebook.page_num (message))
	cbs.update ()

queue = []
current_msg = None
map = Map.Map (0, 0)
map.set_zoom (100000)	# pixels per degree. 1 degree is about 100 km.
map.add_layer (Map.MapLayer (map))
map.add_layer (Map.GridLayer (map, gridcolor))
zones = Map.MarkerLayer (map, zonescolor)
map.add_layer (zones)
objects = Map.MarkerLayer (map, objectscolor)
map.add_layer (objects)
characters = Map.MarkerLayer (map, characterscolor)
map.add_layer (characters)
position = Map.PositionLayer (map, positioncolor)
position.markers = [[(0, 0), [False, True]]]
map.add_layer (position)

def closer (widget, *a):
	widget.hide ()
	return True

opendialog = gtk.FileChooserDialog (title = 'Choose a file to open', action = gtk.FILE_CHOOSER_ACTION_OPEN, buttons = ('Open', 1))
opendialog.connect ('response', open_cb)
opendialog.connect ('close', closer)
savedialog = gtk.FileChooserDialog (title = 'Choose the file to save to', action = gtk.FILE_CHOOSER_ACTION_SAVE, buttons = ('Save', 1))
opendialog.connect ('response', save_cb)
opendialog.connect ('close', closer)
aboutdialog = gtk.AboutDialog ()
aboutdialog.set_name ('Wherpygo')
aboutdialog.set_authors (('Bas Wijnen <wijnen@debian.org>',))
aboutdialog.set_version ('0.4')
aboutdialog.set_comments ('Wherigo cartridge player designed for large screens.')
aboutdialog.connect ('response', closer)
aboutdialog.connect ('close', closer)

win = gtk.Window ()
win.connect ('destroy', file_quit)
vbox = gtk.VBox ()
win.add (vbox)
ui = gtk.UIManager ()
win.add_accel_group (ui.get_accel_group ())
actiongroup = gtk.ActionGroup ('actions')
actiongroup.add_actions ((
	('FileMenu', None, 'File'),
	('HelpMenu', None, 'Help'),
	('new', gtk.STOCK_NEW, None, None, None, file_new),
	('open', gtk.STOCK_OPEN, None, None, None, file_open),
	('save', gtk.STOCK_SAVE, None, None, None, file_save),
	('quit', gtk.STOCK_QUIT, None, None, None, file_quit),
	('about', gtk.STOCK_ABOUT, None, None, None, help_about)))
ui.add_ui_from_string ('''
<ui><menubar>
	<menu name="FileMenu" action="FileMenu">
		<menuitem name="New" action="new"/>
		<menuitem name="Open" action="open"/>
		<!--menuitem name="Save" action="save"/-->
		<menuitem name="Quit" action="quit"/>
	</menu>
	<menu name="HelpMenu" action="HelpMenu">
		<menuitem name="About" action="about"/>
	</menu>
</menubar></ui>''')
ui.insert_action_group (actiongroup)
vbox.pack_start (ui.get_widget ('/menubar'), False)

box = gtk.HPaned ()
if debug:
	tpaned = gtk.VPaned ()
	timers = Timers ()
	tpaned.add1 (timers)
	tpaned.add2 (box)
	vbox.pack_start (tpaned)
else:
	vbox.pack_start (box)
statusbar = gtk.Statusbar ()
vbox.pack_start (statusbar, False)
statuscontext = statusbar.get_context_id ('status')
statusbar.push (statuscontext, '')
notebook = gtk.Notebook ()
notebook.set_show_tabs (True)
notebook.set_tab_pos (gtk.POS_RIGHT)
notebook.set_can_focus (False)
box.add1 (notebook)
mapbox = gtk.VBox ()
box.add2 (mapbox)
mapbox.pack_start (map, True)
posbox = gtk.HBox ()
mapbox.pack_start (posbox, False)
posbox.pack_start (gtk.Label ('Current position:'), False)
lat_label = gtk.Label ()
posbox.pack_start (lat_label, True)
lon_label = gtk.Label ()
posbox.pack_start (lon_label, True)
alt_label = gtk.Label ()
posbox.pack_start (alt_label, True)

def add_screen (screen, label, color, show_size = True):
	ret = screen
	ret.size = 0
	ret.show_size = show_size
	ret.color = color
	ret.tabname = label
	ret.title = gtk.Label ()
	ret.new = False
	notebook.append_page (ret, ret.title)
	update_title (ret)
	return ret

def no_new (book, page, num):
	page = book.get_nth_page (num)
	page.new = False
	update_title (page)
notebook.connect ('switch-page', no_new)

locationscreen = add_screen (Locations (zones), 'Locations', zonescolor)
inventoryscreen = add_screen (Inventory (), 'Inventory', inventorycolor)
environmentscreen = add_screen (Environment (objects, characters), 'Environment', environmentcolor)
taskscreen = add_screen (Tasks (), 'Tasks', taskcolor)
history = add_screen (History (), 'History', historycolor)
log = add_screen (Log (), 'Log', logcolor)
message = add_screen (Detail (), 'Message', messagecolor, show_size = False)
win.show_all ()
map.grab_focus ()
last_screen = notebook.page_num (taskscreen)
if gwcfile:
	notebook.set_current_page (notebook.page_num (message))
else:
	message.hide ()
	notebook.set_current_page (notebook.page_num (locationscreen))

device = GpsPoller ()
def update ():
	# Update Timers. Do this before everything else, so Remaining is set correctly when callbacks are invoked.
	now = time.time ()
	if gameobject:
		for i in gameobject.AllZObjects:
			if isinstance (i, wherigo.ZTimer) and i._target != None:
				i.Remaining = i._target - now
	fp = map.get_force_position ()
	update_all = debug
	if fp is not None:
		p = Position (fp[0], fp[1], wherigo.Player.ObjectLocation.altitude if wherigo.Player.ObjectLocation else 0, 0, 0, 0, 0)
	else:
		p = device.get_pos ()
	if p:
		lat_label.set_text (make_str (p.lat))
		lon_label.set_text (make_str (p.lon))
		alt_label.set_text ('%d m' % p.alt)
		position.markers[0] = [(p.lat, p.lon), [False, True]]
		if not gameobject:
			return True
		wherigo.Player.ObjectLocation = wherigo.ZonePoint (p.lat, p.lon, p.alt)
		for i in gameobject.AllZObjects:
			# Update all object distances and bearings.
			if (isinstance (i, wherigo.ZItem) and i.Container is not wherigo.Player) or isinstance (i, wherigo.ZCharacter):
				if not i.Active or not i.ObjectLocation:
					continue
				i.CurrentDistance, i.CurrentBearing = wherigo.VectorToPoint (wherigo.Player.ObjectLocation, i.ObjectLocation)
			# Update container info, and call OnEnter or OnExit.
			elif isinstance (i, wherigo.Zone):
				if i._active != i.Active:
					# TODO: Doing this here means that OnSetActive fires after the lua callback has returned. Setting a zone active and immediately inactive doesn't make it fire, while it should make it fire twice.
					i._active = i.Active
					if hasattr (i, 'OnSetActive'):
						i.OnSetActive (i)
						update_all = True
				if not i.Active:
					continue
				inside = wherigo.IsPointInZone (wherigo.Player.ObjectLocation, i)
				if inside != i._inside:
					i._inside = inside
					if inside and hasattr (i, 'OnEnter'):
						i.OnEnter (i)
						update_all = True
					elif not inside and hasattr (i, 'OnExit'):
						i.OnExit (i)
						update_all = True
				if inside:
					i.State = 'Inside'
				else:
					# See how close we are.
					i.CurrentDistance, i.CurrentBearing = wherigo.VectorToZone (wherigo.Player.ObjectLocation, i)
					if i.CurrentDistance < i.ProximityRange:
						i.State = 'Proximity'
					elif i.DistanceRange == -1 or i.CurrentDistance < i.DistanceRange:
						i.State = 'Distant'
					else:
						i.State = 'NotInRange'
					if i.State != i._state:
						i._state = i.State
						attr = 'On' + i.State
						if hasattr (i, attr):
							getattr (i, attr) (i)
							update_all = True
	else:
		position.markers[0][1][1] = False
	if update_all:
		cbs.update ()
	cbs.update_stats ()
	map.update ()
	return True
gtk.timeout_add (500, update)

script = None
cbs = CB ()
gameobject = None
file_new (None)
if gwcfile:
	open_cartridge (gwcfile)

gtk.main ()
