#! /usr/bin/env python

import os
import os.path
import sys

# Ignore warnings when we are a binary
if hasattr(sys, 'frozen'):
	import warnings
	warnings.simplefilter('ignore', DeprecationWarning)

from requirements import location
import extra

# wx.Python imports
import wx

# Python Imports
import time
import threading
import traceback

# Local imports
import utils
from version import version
from tp.netlib import failed
from tp.client.threads import NotImportantEvent, Event

from tp.client.ChangeList import ChangeNode


pyid = id

class ThreadExited(Exception):
	pass

from tp.client.threads import ThreadStop
def make(me, thread, method):
	def t(*args, **kw):
		if me.reset:
			raise ThreadStop("Should die now!")
		thread.Call(method, *args, **kw)
	return t

class wxPyEvent(wx.PyEvent, Event):
	def __init__(self):
		self.source = None
		wx.PyEvent.__init__(self)
		Event.__init__(self)

class EventBinder(dict):
	def __init__(self, *args, **kw):
		self.LastEvent = {}
		self.Preconditions = {}
		dict.__init__(self, *args, **kw)

	def Bind(self, event, function, precondition=None):
		if not issubclass(event, Event):
			raise TypeError("First argument must be an event object.")
		if not callable(function):
			raise TypeError("Second argument must be a callable.")
		if not precondition is None and not callable(precondition):
			raise TypeError("precondition must be a callable.")

		name = event.__name__

		if not self.has_key(name):
			self[name] = []
		self[name].append((function, precondition))

	__call__ = Bind

	def Unbind(self, event, function, precondition=None):
		if not issubclass(event, Event):
			raise TypeError("First argument must be an event object.")
		if not callable(function):
			raise TypeError("Argument must be a callable.")
		if not precondition is None and not callable(precondition):
			raise TypeError("precondition must be a callable.")

		name = event.__name__

		if not self.has_key(name) or not function in self[name]:
			raise KeyError("That function is not bound to that argument!")

		self[name].remove((function, precondition))

		if len(self[name]) == 0:
			del self[name]

	def Post(self, event):
		# Make sure that this event has not already passed
		try:
			if self.LastEvent[event.type] > event.time:
				return
		except KeyError:
			pass

		self.LastEvent[event.type] = event.time

		# The people we have already sent the even too
		sentto = [event.source]

		# FIXME: This set does not guarentee the order!
		bases = set([event.__class__])
		while len(bases) > 0:
			# Get the next base class
			cls = bases.pop()
			# Add this class bases to be checked
			bases.update(cls.__bases__)

			name = cls.__name__
			# See if we have anyone who wants to be called for this type.
			if not self.has_key(name):
				continue

			for function, precondition in self[name]:
				# Source is a function
				if function in sentto:
					continue

				# If source is an class, get the function off the class so we can check
				if hasattr(event.source, function.__name__) and getattr(event.source, function.__name__) == function:
					continue

				try:
					if not precondition is None and not precondition():
						continue

					function(event)
				except Exception, e:
					utils.do_traceback()
				
				sentto.append(function)

		#print "I sent the following event (from %r) %r to %r" % (event.source, event, sentto)

class GUI(wx.App):
	## These are window events
	class ShowWindowEvent(wxPyEvent):
		"""\
		Raised when the the windows are showed.
		"""
		pass

	class SelectObjectEvent(wxPyEvent):
		"""\
		Raised when an object is selected.
		"""
		def __init__(self, id):
			wxPyEvent.__init__(self)

			assert id >= 0
			self.id = id
	
		def __str__(self):
			return "<SelectObjectEvent id=%s>" % self.id
		__repr__ = __str__
	
	class PreviewObjectEvent(SelectObjectEvent):
		pass

	class SelectPositionEvent(wxPyEvent):
		"""\
		Raised when a position is selected.
		"""
		def __init__(self, pos):
			wxPyEvent.__init__(self)

			self.x, self.y, self.z = pos
		
		def __str__(self):
			return "<SelectPositionEvent (%s, %s %s)>" % (self.x, self.y, self.z)
		__repr__ = __str__
	
	class SelectOrderEvent(wxPyEvent):
		"""\
		Raised when an order is selected.
		"""
		def __init__(self, id, nodes):
			wxPyEvent.__init__(self)

			self.id = id
			if not hasattr(nodes, '__getitem__'):
				nodes = [nodes]

			for node in nodes:
				assert isinstance(node, ChangeNode)

			self.nodes = nodes
		
		def __str__(self):
			return "<SelectOrderEvent(%x) id=%s nodes=%s>" % (pyid(self), self.id, self.nodes)
		__repr__ = __str__

	class DirtyOrderEvent(wxPyEvent):
		"""\
		Raised when an order value changes but hasn't been saved yet.
		"""
		def __init__(self, id, node):
			wxPyEvent.__init__(self)

			self.id = id
			self.nodes = [node]
			self.order = node.pending
		
		def __str__(self):
			return "<DirtyOrderEvent id=%s nodes=%s>" % (self.id, self.nodes)
		__repr__ = __str__

	######################################
	def __init__(self, application):
		if wx.Platform in ('__WXMSW__', '__WXMAC__'):
			#wx.App.__init__(self, redirect = True, filename = 'errors')
			wx.App.__init__(self, redirect=False)
		else:
			wx.App.__init__(self)

		self.Binder = EventBinder()

		############
		# Setup the translation stuff...
		import gettext
		# Hack to get the locale directory
		basepath  = os.path.abspath(location())
		localedir = os.path.join(basepath, "locale")

		langid = wx.LANGUAGE_DEFAULT    # use OS default; or use LANGUAGE_JAPANESE, etc.
		domain = "tpclient-pywx"        # the translation file is tpclient-pywx.mo

		# Set locale for wxWidgets
		mylocale = wx.Locale(langid)
		mylocale.AddCatalogLookupPathPrefix(localedir)
		mylocale.AddCatalog(domain)

		# Set up Python's gettext
		mytranslation = gettext.translation(domain, localedir, [mylocale.GetCanonicalName()], fallback = True)
		mytranslation.install()

		__builtins__._ = wx.GetTranslation
		##############
		self.start = self.MainLoop

		self.application = application
		self.exit = False

		self.current = None

		# Show the splash screen
		from windows.winSplash import winSplash
		self.splash = winSplash(application)
		self.Show(self.splash)

	def Create(self):
		self.count = 0

		self.UpdateUILock = threading.Lock()
		self.GUIThread = threading.currentThread()

		application = self.application

		try:
			wx.InitAllImageHandlers()
			
			# Load the other main windows
			from windows.winMain import winMain
			self.main = winMain(application)

			from windows.winConnect import winConnect
			self.connectto = winConnect(application)

			from windows.winServerBrowser import winServerBrowser
			self.servers = winServerBrowser(application)

			from windows.winAccount import winAccount
			self.account = winAccount(application)

			from windows.winUpdate import winUpdate
			self.update = winUpdate(application)

			self.windows = (self.main, self.connectto, self.update)
		
			from windows.winConfig import winConfig
			self.config = winConfig(application, self.windows)

		except:
			utils.do_traceback()
		self.Call(self.Show, self.connectto)

	def __str__(self):
		return "<GUIThread(wxThread, %s)>" % hex(pyid(self))

	def Cleanup(self):
		for window in self.windows:
			window.Close()
		while self.application.network.isAlive() or self.application.media.isAlive():
			time.sleep(0.1)
		sys.exit()

	# Config Functions ------------------------------------------------
	def ConfigDisplay(self):
		"""\
		Display the configuration window.
		"""
		self.config.Show(True)

	def ConfigSave(self):
		"""\
		Display the configuration window.
		"""
		config = {}
		for window in self.windows:
			config[window.title] = window.ConfigSave()
		return config

	def ConfigLoad(self, config):
		"""\
		Display the configuration window.
		"""
		for window in self.windows:
			window.ConfigLoad(config.get(window.title, {}))

	# Inter-Thread Functions ------------------------------------------------
	def Call(self, method, *args, **kw):
		"""\
		Call a method in this thread.
		"""
		if (threading.currentThread() == self.GUIThread):
			wx.CallAfter(method, *args, **kw)
		else:
			self.UpdateUILock.acquire()

			wx.CallAfter(method, *args, **kw)

			self.count += 1
			if self.count % 5 == 0:
				wx.CallAfter(self.UpdateUI)
			else:
				self.UpdateUILock.release()

	def UpdateUI(self):
		wx.Yield()
		self.UpdateUILock.release()

	def Post(self, event):
		"""
		Post an Event the current window.
		"""
		func = 'On' + event.type
		if hasattr(self, func):
			try:
				getattr(self, func)(event)
			except Exception, e:
				utils.do_traceback()

		self.Binder.Post(event)

	def Show(self, window):
		"""
		Change to a certain main window.
		"""
		if window == self.current:
			self.current.Show()
			return

		if self.current != None:
			try:
				self.current.Hide()
			except Exception, e:
				utils.do_traceback()

		if isinstance(window, wx.Window):
			self.SetTopWindow(window)

		self.current = window
		r = self.current.Show()

		self.Post(self.ShowWindowEvent())
		return r

	def OnNetworkFailure(self, evt):
		if hasattr(self.current, "OnNetworkFailure"):
			return
		else:
			print "OnNetworkFailure", evt
			self.Show(self.connectto)

			# When the network fails pop-up a dialog then go to the connectto screen
			dlg = wx.MessageDialog(self.application.gui.current, unicode(evt), _("Network Error"), wx.OK|wx.ICON_ERROR)
			dlg.ShowModal()
			dlg.Destroy()

from tp.client.threads import NetworkThread
class Network(NetworkThread):
	def ConnectTo(self, host, username, password, debug=False):
		"""\
		Connect to a given host using a certain username and password.
		"""
		# FIXME: This should be sending events really
		gui = self.application.gui
		up = make(self, gui, gui.update.Callback)

		# Show the Update window
		gui.Call(gui.Show, gui.update)
		if NetworkThread.ConnectTo(self, host, username, password, debug, up, cs="tpclient-pywx/%s.%s.%s" % version):
			self.CacheUpdate()

			media = self.application.media
			media.Call(media.ConnectTo, host, username)
		else:
			print "Connecting to failed"

	def CacheUpdate(self):
		gui = self.application.gui
		up = make(self, gui, gui.update.Callback)

		# Show the update window
		gui.Call(gui.Show, gui.update)
		while gui.current != gui.update:
			time.sleep(0.1)

		up("connecting", "start", "Connecting...")
		up("connecting", "finished", "")
		NetworkThread.CacheUpdate(self, up)
		up("finishing", "finished", "")

		self.EOTUpdate()

	def EOTUpdate(self):
		# FIXME: This should go away...
		# FIXME: This is bad :/
		gui = self.application.gui

		timeRemaining = self.connection.time(), time.time()

		if failed(timeRemaining[0]):
			return
		gui.Call( gui.main.statusbar.SetEndTime, timeRemaining[0]+timeRemaining[1] )

from tp.client.threads import Application as ClientApplication
class Application(ClientApplication):
	# We used custom GUI and Network threads
	GUIClass = GUI
	NetworkClass = Network

	# We just use the default threads from libtpclient-py
	from tp.client.threads import MediaThread, FinderThread
	MediaClass = MediaThread
	FinderClass = FinderThread

	ConfigFile = "pywx_preferences"

	def __init__(self, url=None):
		# self.gui       is initialised from this Application.GUIClass
		# self.network   is initialised from this Application.NetworkClass
		# self.media     is initialised from this Application.MediaClass
		# self.finder    is initialised from this Application.FinderClass
		ClientApplication.__init__(self)
		if url != None:
			self.gui.connectto.ShowURL(url)		

	def Run(self):
		"""\
		Set the application running.
		"""
		self.gui.Show(self.gui.connectto)
		ClientApplication.Run(self)

	def ConfigDisplay(self):
		"""\
		Pop-up the configuration window.
		"""
		self.gui.ConfigDisplay()

if __name__ == '__main__':
	try:
		if hasattr(sys,"frozen") and sys.frozen == "windows_exe":
			pwd=os.path.dirname(os.path.join(os.path.abspath(sys.executable)))
		else:
			pwd=os.path.dirname(os.path.join(os.path.abspath(__file__)))
		os.chdir(pwd)
		
		url = None
		if len(sys.argv) > 1:
			url = sys.argv[1]

		app = Application(url)
		app.Run()
	finally:
		utils.do_traceback()
